Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
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
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
f623e429
Commit
f623e429
authored
Jun 18, 2013
by
Peter Baratta
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix formatting of docstrings; add more docstrings
parent
b68e1e20
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
117 additions
and
51 deletions
+117
-51
common/lib/xmodule/xmodule/capa_module.py
+105
-47
common/lib/xmodule/xmodule/tests/test_capa_module.py
+12
-4
No files found.
common/lib/xmodule/xmodule/capa_module.py
View file @
f623e429
...
@@ -47,7 +47,13 @@ def randomization_bin(seed, problem_id):
...
@@ -47,7 +47,13 @@ def randomization_bin(seed, problem_id):
class
Randomization
(
String
):
class
Randomization
(
String
):
"""
Define a field to store how to randomize a problem.
"""
def
from_json
(
self
,
value
):
def
from_json
(
self
,
value
):
"""
For backward compatability?
"""
if
value
in
(
""
,
"true"
):
if
value
in
(
""
,
"true"
):
return
"always"
return
"always"
elif
value
==
"false"
:
elif
value
==
"false"
:
...
@@ -58,13 +64,22 @@ class Randomization(String):
...
@@ -58,13 +64,22 @@ class Randomization(String):
class
ComplexEncoder
(
json
.
JSONEncoder
):
class
ComplexEncoder
(
json
.
JSONEncoder
):
"""
Extend the JSON encoder to correctly handle complex numbers
"""
def
default
(
self
,
obj
):
def
default
(
self
,
obj
):
"""
Print a nicely formatted complex number, or default to the JSON encoder
"""
if
isinstance
(
obj
,
complex
):
if
isinstance
(
obj
,
complex
):
return
u"{real:.7g}{imag:+.7g}*j"
.
format
(
real
=
obj
.
real
,
imag
=
obj
.
imag
)
return
u"{real:.7g}{imag:+.7g}*j"
.
format
(
real
=
obj
.
real
,
imag
=
obj
.
imag
)
return
json
.
JSONEncoder
.
default
(
self
,
obj
)
return
json
.
JSONEncoder
.
default
(
self
,
obj
)
class
CapaFields
(
object
):
class
CapaFields
(
object
):
"""
Define the possible fields for a Capa problem
"""
attempts
=
Integer
(
help
=
"Number of attempts taken by the student on this problem"
,
attempts
=
Integer
(
help
=
"Number of attempts taken by the student on this problem"
,
default
=
0
,
scope
=
Scope
.
user_state
)
default
=
0
,
scope
=
Scope
.
user_state
)
max_attempts
=
Integer
(
max_attempts
=
Integer
(
...
@@ -130,12 +145,12 @@ class CapaFields(object):
...
@@ -130,12 +145,12 @@ class CapaFields(object):
class
CapaModule
(
CapaFields
,
XModule
):
class
CapaModule
(
CapaFields
,
XModule
):
'''
"""
An XModule implementing LonCapa format problems, implemented by way of
An XModule implementing LonCapa format problems, implemented by way of
capa.capa_problem.LoncapaProblem
capa.capa_problem.LoncapaProblem
CapaModule.__init__ takes the same arguments as xmodule.x_module:XModule.__init__
CapaModule.__init__ takes the same arguments as xmodule.x_module:XModule.__init__
'''
"""
icon_class
=
'problem'
icon_class
=
'problem'
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/capa/display.coffee'
),
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/capa/display.coffee'
),
...
@@ -150,7 +165,9 @@ class CapaModule(CapaFields, XModule):
...
@@ -150,7 +165,9 @@ class CapaModule(CapaFields, XModule):
css
=
{
'scss'
:
[
resource_string
(
__name__
,
'css/capa/display.scss'
)]}
css
=
{
'scss'
:
[
resource_string
(
__name__
,
'css/capa/display.scss'
)]}
def
__init__
(
self
,
*
args
,
**
kwargs
):
def
__init__
(
self
,
*
args
,
**
kwargs
):
""" Accepts the same arguments as xmodule.x_module:XModule.__init__ """
"""
Accepts the same arguments as xmodule.x_module:XModule.__init__
"""
XModule
.
__init__
(
self
,
*
args
,
**
kwargs
)
XModule
.
__init__
(
self
,
*
args
,
**
kwargs
)
due_date
=
self
.
due
due_date
=
self
.
due
...
@@ -211,7 +228,9 @@ class CapaModule(CapaFields, XModule):
...
@@ -211,7 +228,9 @@ class CapaModule(CapaFields, XModule):
assert
self
.
seed
is
not
None
assert
self
.
seed
is
not
None
def
choose_new_seed
(
self
):
def
choose_new_seed
(
self
):
"""Choose a new seed."""
"""
Choose a new seed.
"""
if
self
.
rerandomize
==
'never'
:
if
self
.
rerandomize
==
'never'
:
self
.
seed
=
1
self
.
seed
=
1
elif
self
.
rerandomize
==
"per_student"
and
hasattr
(
self
.
system
,
'seed'
):
elif
self
.
rerandomize
==
"per_student"
and
hasattr
(
self
.
system
,
'seed'
):
...
@@ -225,6 +244,9 @@ class CapaModule(CapaFields, XModule):
...
@@ -225,6 +244,9 @@ class CapaModule(CapaFields, XModule):
self
.
seed
%=
MAX_RANDOMIZATION_BINS
self
.
seed
%=
MAX_RANDOMIZATION_BINS
def
new_lcp
(
self
,
state
,
text
=
None
):
def
new_lcp
(
self
,
state
,
text
=
None
):
"""
Generate a new Loncapa Problem
"""
if
text
is
None
:
if
text
is
None
:
text
=
self
.
data
text
=
self
.
data
...
@@ -237,6 +259,9 @@ class CapaModule(CapaFields, XModule):
...
@@ -237,6 +259,9 @@ class CapaModule(CapaFields, XModule):
)
)
def
get_state_for_lcp
(
self
):
def
get_state_for_lcp
(
self
):
"""
Give a dictionary holding the state of the module
"""
return
{
return
{
'done'
:
self
.
done
,
'done'
:
self
.
done
,
'correct_map'
:
self
.
correct_map
,
'correct_map'
:
self
.
correct_map
,
...
@@ -246,6 +271,9 @@ class CapaModule(CapaFields, XModule):
...
@@ -246,6 +271,9 @@ class CapaModule(CapaFields, XModule):
}
}
def
set_state_from_lcp
(
self
):
def
set_state_from_lcp
(
self
):
"""
Set the module's state from the settings in `self.lcp`
"""
lcp_state
=
self
.
lcp
.
get_state
()
lcp_state
=
self
.
lcp
.
get_state
()
self
.
done
=
lcp_state
[
'done'
]
self
.
done
=
lcp_state
[
'done'
]
self
.
correct_map
=
lcp_state
[
'correct_map'
]
self
.
correct_map
=
lcp_state
[
'correct_map'
]
...
@@ -254,26 +282,36 @@ class CapaModule(CapaFields, XModule):
...
@@ -254,26 +282,36 @@ class CapaModule(CapaFields, XModule):
self
.
seed
=
lcp_state
[
'seed'
]
self
.
seed
=
lcp_state
[
'seed'
]
def
get_score
(
self
):
def
get_score
(
self
):
"""
Access the problem's score
"""
return
self
.
lcp
.
get_score
()
return
self
.
lcp
.
get_score
()
def
max_score
(
self
):
def
max_score
(
self
):
"""
Access the problem's max score
"""
return
self
.
lcp
.
get_max_score
()
return
self
.
lcp
.
get_max_score
()
def
get_progress
(
self
):
def
get_progress
(
self
):
''' For now, just return score / max_score
"""
'''
For now, just return score / max_score
"""
d
=
self
.
get_score
()
d
=
self
.
get_score
()
score
=
d
[
'score'
]
score
=
d
[
'score'
]
total
=
d
[
'total'
]
total
=
d
[
'total'
]
if
total
>
0
:
if
total
>
0
:
try
:
try
:
return
Progress
(
score
,
total
)
return
Progress
(
score
,
total
)
except
Exception
:
except
(
TypeError
,
ValueError
)
:
log
.
exception
(
"Got bad progress"
)
log
.
exception
(
"Got bad progress"
)
return
None
return
None
return
None
return
None
def
get_html
(
self
):
def
get_html
(
self
):
"""
Return some html with data about the module
"""
return
self
.
system
.
render_template
(
'problem_ajax.html'
,
{
return
self
.
system
.
render_template
(
'problem_ajax.html'
,
{
'element_id'
:
self
.
location
.
html_id
(),
'element_id'
:
self
.
location
.
html_id
(),
'id'
:
self
.
id
,
'id'
:
self
.
id
,
...
@@ -284,6 +322,7 @@ class CapaModule(CapaFields, XModule):
...
@@ -284,6 +322,7 @@ class CapaModule(CapaFields, XModule):
def
check_button_name
(
self
):
def
check_button_name
(
self
):
"""
"""
Determine the name for the "check" button.
Determine the name for the "check" button.
Usually it is just "Check", but if this is the student's
Usually it is just "Check", but if this is the student's
final attempt, change the name to "Final Check"
final attempt, change the name to "Final Check"
"""
"""
...
@@ -369,12 +408,12 @@ class CapaModule(CapaFields, XModule):
...
@@ -369,12 +408,12 @@ class CapaModule(CapaFields, XModule):
def
handle_problem_html_error
(
self
,
err
):
def
handle_problem_html_error
(
self
,
err
):
"""
"""
Change our problem to a dummy problem containing
Create a dummy problem to represent any errors.
a warning message to display to users.
Returns the HTML to show to users
Change our problem to a dummy problem containing a warning message to
display to users. Returns the HTML to show to users
*err*
is the Exception encountered while rendering the problem HTML.
`err`
is the Exception encountered while rendering the problem HTML.
"""
"""
log
.
exception
(
err
)
log
.
exception
(
err
)
...
@@ -434,8 +473,12 @@ class CapaModule(CapaFields, XModule):
...
@@ -434,8 +473,12 @@ class CapaModule(CapaFields, XModule):
return
html
return
html
def
get_problem_html
(
self
,
encapsulate
=
True
):
def
get_problem_html
(
self
,
encapsulate
=
True
):
'''Return html for the problem. Adds check, reset, save buttons
"""
as necessary based on the problem config and state.'''
Return html for the problem.
Adds check, reset, save buttons as necessary based on the problem config
and state.
"""
try
:
try
:
html
=
self
.
lcp
.
get_html
()
html
=
self
.
lcp
.
get_html
()
...
@@ -480,15 +523,16 @@ class CapaModule(CapaFields, XModule):
...
@@ -480,15 +523,16 @@ class CapaModule(CapaFields, XModule):
return
self
.
system
.
replace_urls
(
html
)
return
self
.
system
.
replace_urls
(
html
)
def
handle_ajax
(
self
,
dispatch
,
get
):
def
handle_ajax
(
self
,
dispatch
,
get
):
'''
"""
This is called by courseware.module_render, to handle an AJAX call.
This is called by courseware.module_render, to handle an AJAX call.
"get" is request.POST.
`get` is request.POST.
Returns a json dictionary:
Returns a json dictionary:
{ 'progress_changed' : True/False,
{ 'progress_changed' : True/False,
'progress' : 'none'/'in_progress'/'done',
'progress' : 'none'/'in_progress'/'done',
<other request-specific values here > }
<other request-specific values here > }
'''
"""
handlers
=
{
handlers
=
{
'problem_get'
:
self
.
get_problem
,
'problem_get'
:
self
.
get_problem
,
'problem_check'
:
self
.
check_problem
,
'problem_check'
:
self
.
check_problem
,
...
@@ -527,7 +571,9 @@ class CapaModule(CapaFields, XModule):
...
@@ -527,7 +571,9 @@ class CapaModule(CapaFields, XModule):
datetime
.
datetime
.
now
(
UTC
())
>
self
.
close_date
)
datetime
.
datetime
.
now
(
UTC
())
>
self
.
close_date
)
def
closed
(
self
):
def
closed
(
self
):
''' Is the student still allowed to submit answers? '''
"""
Is the student still allowed to submit answers?
"""
if
self
.
max_attempts
is
not
None
and
self
.
attempts
>=
self
.
max_attempts
:
if
self
.
max_attempts
is
not
None
and
self
.
attempts
>=
self
.
max_attempts
:
return
True
return
True
if
self
.
is_past_due
():
if
self
.
is_past_due
():
...
@@ -546,18 +592,24 @@ class CapaModule(CapaFields, XModule):
...
@@ -546,18 +592,24 @@ class CapaModule(CapaFields, XModule):
return
self
.
lcp
.
done
return
self
.
lcp
.
done
def
is_attempted
(
self
):
def
is_attempted
(
self
):
"""Used by conditional module"""
"""
Has the problem been attempted?
used by conditional module
"""
return
self
.
attempts
>
0
return
self
.
attempts
>
0
def
is_correct
(
self
):
def
is_correct
(
self
):
"""True if full points"""
"""
True iff full points
"""
d
=
self
.
get_score
()
d
=
self
.
get_score
()
return
d
[
'score'
]
==
d
[
'total'
]
return
d
[
'score'
]
==
d
[
'total'
]
def
answer_available
(
self
):
def
answer_available
(
self
):
'''
"""
Is the user allowed to see an answer?
Is the user allowed to see an answer?
'''
"""
if
self
.
showanswer
==
''
:
if
self
.
showanswer
==
''
:
return
False
return
False
elif
self
.
showanswer
==
"never"
:
elif
self
.
showanswer
==
"never"
:
...
@@ -589,7 +641,7 @@ class CapaModule(CapaFields, XModule):
...
@@ -589,7 +641,7 @@ class CapaModule(CapaFields, XModule):
Delivers grading response (e.g. from asynchronous code checking) to
Delivers grading response (e.g. from asynchronous code checking) to
the capa problem, so its score can be updated
the capa problem, so its score can be updated
'get' must have a field 'response'
which is a string that contains the
`get` must have a field `response`
which is a string that contains the
grader's response
grader's response
No ajax return is needed. Return empty dict.
No ajax return is needed. Return empty dict.
...
@@ -603,7 +655,7 @@ class CapaModule(CapaFields, XModule):
...
@@ -603,7 +655,7 @@ class CapaModule(CapaFields, XModule):
return
dict
()
# No AJAX return is needed
return
dict
()
# No AJAX return is needed
def
handle_ungraded_response
(
self
,
get
):
def
handle_ungraded_response
(
self
,
get
):
'''
"""
Delivers a response from the XQueue to the capa problem
Delivers a response from the XQueue to the capa problem
The score of the problem will not be updated
The score of the problem will not be updated
...
@@ -616,7 +668,7 @@ class CapaModule(CapaFields, XModule):
...
@@ -616,7 +668,7 @@ class CapaModule(CapaFields, XModule):
empty dictionary
empty dictionary
No ajax return is needed, so an empty dict is returned
No ajax return is needed, so an empty dict is returned
'''
"""
queuekey
=
get
[
'queuekey'
]
queuekey
=
get
[
'queuekey'
]
score_msg
=
get
[
'xqueue_body'
]
score_msg
=
get
[
'xqueue_body'
]
# pass along the xqueue message to the problem
# pass along the xqueue message to the problem
...
@@ -625,25 +677,25 @@ class CapaModule(CapaFields, XModule):
...
@@ -625,25 +677,25 @@ class CapaModule(CapaFields, XModule):
return
dict
()
return
dict
()
def
handle_input_ajax
(
self
,
get
):
def
handle_input_ajax
(
self
,
get
):
'''
"""
Handle ajax calls meant for a particular input in the problem
Handle ajax calls meant for a particular input in the problem
Args:
Args:
- get (dict) - data that should be passed to the input
- get (dict) - data that should be passed to the input
Returns:
Returns:
- dict containing the response from the input
- dict containing the response from the input
'''
"""
response
=
self
.
lcp
.
handle_input_ajax
(
get
)
response
=
self
.
lcp
.
handle_input_ajax
(
get
)
# save any state changes that may occur
# save any state changes that may occur
self
.
set_state_from_lcp
()
self
.
set_state_from_lcp
()
return
response
return
response
def
get_answer
(
self
,
get
):
def
get_answer
(
self
,
get
):
'''
"""
For the "show answer" button.
For the "show answer" button.
Returns the answers: {'answers' : answers}
Returns the answers: {'answers' : answers}
'''
"""
event_info
=
dict
()
event_info
=
dict
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
self
.
system
.
track_function
(
'showanswer'
,
event_info
)
self
.
system
.
track_function
(
'showanswer'
,
event_info
)
...
@@ -669,40 +721,44 @@ class CapaModule(CapaFields, XModule):
...
@@ -669,40 +721,44 @@ class CapaModule(CapaFields, XModule):
# Figure out if we should move these to capa_problem?
# Figure out if we should move these to capa_problem?
def
get_problem
(
self
,
get
):
def
get_problem
(
self
,
get
):
''' Return results of get_problem_html, as a simple dict for json-ing.
"""
Return results of get_problem_html, as a simple dict for json-ing.
{ 'html': <the-html> }
{ 'html': <the-html> }
Used if we want to reconfirm we have the right thing e.g. after
Used if we want to reconfirm we have the right thing e.g. after
several AJAX calls.
several AJAX calls.
'''
"""
return
{
'html'
:
self
.
get_problem_html
(
encapsulate
=
False
)}
return
{
'html'
:
self
.
get_problem_html
(
encapsulate
=
False
)}
@staticmethod
@staticmethod
def
make_dict_of_responses
(
get
):
def
make_dict_of_responses
(
get
):
'''Make dictionary of student responses (aka "answers")
"""
get is POST dictionary (Django QueryDict).
Make dictionary of student responses (aka "answers")
`get` is POST dictionary (Django QueryDict).
The
*get*
dict has keys of the form 'x_y', which are mapped
The
`get`
dict has keys of the form 'x_y', which are mapped
to key 'y' in the returned dict. For example,
to key 'y' in the returned dict. For example,
'input_1_2_3' would be mapped to '1_2_3' in the returned dict.
'input_1_2_3' would be mapped to '1_2_3' in the returned dict.
Some inputs always expect a list in the returned dict
Some inputs always expect a list in the returned dict
(e.g. checkbox inputs). The convention is that
(e.g. checkbox inputs). The convention is that
keys in the
*get*
dict that end with '[]' will always
keys in the
`get`
dict that end with '[]' will always
have list values in the returned dict.
have list values in the returned dict.
For example, if the
*get*
dict contains {'input_1[]': 'test' }
For example, if the
`get`
dict contains {'input_1[]': 'test' }
then the output dict would contain {'1': ['test'] }
then the output dict would contain {'1': ['test'] }
(the value is a list).
(the value is a list).
Raises an exception if:
Raises an exception if:
A key in the *get* dictionary does not contain >= 1 underscores
-A key in the `get` dictionary does not contain at least one underscore
(e.g. "input" is invalid;
"input_1" is valid)
(e.g. "input" is invalid, but
"input_1" is valid)
Two keys end up with the same name in the returned dict.
-
Two keys end up with the same name in the returned dict.
(e.g. 'input_1' and 'input_1[]', which both get mapped
(e.g. 'input_1' and 'input_1[]', which both get mapped to 'input_1'
to 'input_1'
in the returned dict)
in the returned dict)
'''
"""
answers
=
dict
()
answers
=
dict
()
for
key
in
get
:
for
key
in
get
:
...
@@ -749,12 +805,13 @@ class CapaModule(CapaFields, XModule):
...
@@ -749,12 +805,13 @@ class CapaModule(CapaFields, XModule):
})
})
def
check_problem
(
self
,
get
):
def
check_problem
(
self
,
get
):
''' Checks whether answers to a problem are correct, and
"""
returns a map of correct/incorrect answers:
Checks whether answers to a problem are correct
Returns a map of correct/incorrect answers:
{'success' : 'correct' | 'incorrect' | AJAX alert msg string,
{'success' : 'correct' | 'incorrect' | AJAX alert msg string,
'contents' : html}
'contents' : html}
'''
"""
event_info
=
dict
()
event_info
=
dict
()
event_info
[
'state'
]
=
self
.
lcp
.
get_state
()
event_info
[
'state'
]
=
self
.
lcp
.
get_state
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
...
@@ -958,7 +1015,8 @@ class CapaModule(CapaFields, XModule):
...
@@ -958,7 +1015,8 @@ class CapaModule(CapaFields, XModule):
'msg'
:
msg
}
'msg'
:
msg
}
def
reset_problem
(
self
,
get
):
def
reset_problem
(
self
,
get
):
''' Changes problem state to unfinished -- removes student answers,
"""
Changes problem state to unfinished -- removes student answers,
and causes problem to rerender itself.
and causes problem to rerender itself.
Returns a dictionary of the form:
Returns a dictionary of the form:
...
@@ -966,8 +1024,8 @@ class CapaModule(CapaFields, XModule):
...
@@ -966,8 +1024,8 @@ class CapaModule(CapaFields, XModule):
'html': Problem HTML string }
'html': Problem HTML string }
If an error occurs, the dictionary will also have an
If an error occurs, the dictionary will also have an
'error'
key containing an error message.
`error`
key containing an error message.
'''
"""
event_info
=
dict
()
event_info
=
dict
()
event_info
[
'old_state'
]
=
self
.
lcp
.
get_state
()
event_info
[
'old_state'
]
=
self
.
lcp
.
get_state
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
...
...
common/lib/xmodule/xmodule/tests/test_capa_module.py
View file @
f623e429
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
"""Tests of the Capa XModule"""
"""
Tests of the Capa XModule
"""
#pylint: disable=C0111
#pylint: disable=C0111
#pylint: disable=R0904
#pylint: disable=R0904
#pylint: disable=C0103
#pylint: disable=C0103
...
@@ -48,12 +50,16 @@ class CapaFactory(object):
...
@@ -48,12 +50,16 @@ class CapaFactory(object):
@staticmethod
@staticmethod
def
input_key
():
def
input_key
():
""" Return the input key to use when passing GET parameters """
"""
Return the input key to use when passing GET parameters
"""
return
(
"input_"
+
CapaFactory
.
answer_key
())
return
(
"input_"
+
CapaFactory
.
answer_key
())
@staticmethod
@staticmethod
def
answer_key
():
def
answer_key
():
""" Return the key stored in the capa problem answer dict """
"""
Return the key stored in the capa problem answer dict
"""
return
(
"-"
.
join
([
'i4x'
,
'edX'
,
'capa_test'
,
'problem'
,
return
(
"-"
.
join
([
'i4x'
,
'edX'
,
'capa_test'
,
'problem'
,
'SampleProblem
%
d'
%
CapaFactory
.
num
])
+
'SampleProblem
%
d'
%
CapaFactory
.
num
])
+
"_2_1"
)
"_2_1"
)
...
@@ -362,7 +368,9 @@ class CapaModuleTest(unittest.TestCase):
...
@@ -362,7 +368,9 @@ class CapaModuleTest(unittest.TestCase):
result
=
CapaModule
.
make_dict_of_responses
(
invalid_get_dict
)
result
=
CapaModule
.
make_dict_of_responses
(
invalid_get_dict
)
def
_querydict_from_dict
(
self
,
param_dict
):
def
_querydict_from_dict
(
self
,
param_dict
):
""" Create a Django QueryDict from a Python dictionary """
"""
Create a Django QueryDict from a Python dictionary
"""
# QueryDict objects are immutable by default, so we make
# QueryDict objects are immutable by default, so we make
# a copy that we can update.
# a copy that we can update.
...
...
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