Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
D
django-rest-framework
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
django-rest-framework
Commits
4d126796
Commit
4d126796
authored
May 10, 2011
by
Tom Christie
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
More tests, getting new serialization into resource
parent
a2575c11
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
174 additions
and
36 deletions
+174
-36
djangorestframework/authentication.py
+2
-2
djangorestframework/mixins.py
+16
-7
djangorestframework/renderers.py
+16
-9
djangorestframework/resource.py
+77
-4
djangorestframework/response.py
+14
-5
djangorestframework/status.py
+4
-2
djangorestframework/templates/renderer.html
+1
-1
djangorestframework/tests/resource.py
+32
-0
djangorestframework/validators.py
+12
-6
No files found.
djangorestframework/authentication.py
View file @
4d126796
...
@@ -3,7 +3,7 @@ The ``authentication`` module provides a set of pluggable authentication classes
...
@@ -3,7 +3,7 @@ The ``authentication`` module provides a set of pluggable authentication classes
Authentication behavior is provided by adding the ``AuthMixin`` class to a ``View`` .
Authentication behavior is provided by adding the ``AuthMixin`` class to a ``View`` .
The set of authentication methods which are used is then specified by setting
The set of authentication methods which are used is then specified by setting
the
``authentication`` attribute on the ``View`` class, and listing a set of authentication classes.
``authentication`` attribute on the ``View`` class, and listing a set of authentication classes.
"""
"""
...
@@ -81,7 +81,7 @@ class UserLoggedInAuthenticaton(BaseAuthenticaton):
...
@@ -81,7 +81,7 @@ class UserLoggedInAuthenticaton(BaseAuthenticaton):
"""
"""
def
authenticate
(
self
,
request
):
def
authenticate
(
self
,
request
):
# TODO: Switch this back to request.POST, and let MultiPartParser deal with the consequences.
# TODO: Switch this back to request.POST, and let
FormParser/
MultiPartParser deal with the consequences.
if
getattr
(
request
,
'user'
,
None
)
and
request
.
user
.
is_active
:
if
getattr
(
request
,
'user'
,
None
)
and
request
.
user
.
is_active
:
# If this is a POST request we enforce CSRF validation.
# If this is a POST request we enforce CSRF validation.
if
request
.
method
.
upper
()
==
'POST'
:
if
request
.
method
.
upper
()
==
'POST'
:
...
...
djangorestframework/mixins.py
View file @
4d126796
...
@@ -33,7 +33,7 @@ __all__ = (
...
@@ -33,7 +33,7 @@ __all__ = (
class
RequestMixin
(
object
):
class
RequestMixin
(
object
):
"""
"""
Mixin class to provide request parsing behavio
u
r.
Mixin class to provide request parsing behavior.
"""
"""
USE_FORM_OVERLOADING
=
True
USE_FORM_OVERLOADING
=
True
...
@@ -93,6 +93,13 @@ class RequestMixin(object):
...
@@ -93,6 +93,13 @@ class RequestMixin(object):
if
content_length
==
0
:
if
content_length
==
0
:
return
None
return
None
elif
hasattr
(
request
,
'read'
):
elif
hasattr
(
request
,
'read'
):
# UPDATE BASED ON COMMENT BELOW:
#
# Yup, this was a bug in Django - fixed and waiting check in - see ticket 15785.
# http://code.djangoproject.com/ticket/15785
#
# COMMENT:
#
# It's not at all clear if this needs to be byte limited or not.
# It's not at all clear if this needs to be byte limited or not.
# Maybe I'm just being dumb but it looks to me like there's some issues
# Maybe I'm just being dumb but it looks to me like there's some issues
# with that in Django.
# with that in Django.
...
@@ -117,8 +124,6 @@ class RequestMixin(object):
...
@@ -117,8 +124,6 @@ class RequestMixin(object):
#except (ValueError, TypeError):
#except (ValueError, TypeError):
# content_length = 0
# content_length = 0
# self._stream = LimitedStream(request, content_length)
# self._stream = LimitedStream(request, content_length)
#
# UPDATE: http://code.djangoproject.com/ticket/15785
self
.
_stream
=
request
self
.
_stream
=
request
else
:
else
:
self
.
_stream
=
StringIO
(
request
.
raw_post_data
)
self
.
_stream
=
StringIO
(
request
.
raw_post_data
)
...
@@ -290,11 +295,15 @@ class ResponseMixin(object):
...
@@ -290,11 +295,15 @@ class ResponseMixin(object):
return
resp
return
resp
# TODO: This should be simpler now.
# Add a handles_response() to the renderer, then iterate through the
# acceptable media types, ordered by how specific they are,
# calling handles_response on each renderer.
def
_determine_renderer
(
self
,
request
):
def
_determine_renderer
(
self
,
request
):
"""
"""
Return the appropriate renderer for the output, given the client's 'Accept' header,
Return the appropriate renderer for the output, given the client's 'Accept' header,
and the content types that this mixin knows how to serve.
and the content types that this mixin knows how to serve.
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
See: RFC 2616, Section 14 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
"""
"""
...
@@ -321,7 +330,7 @@ class ResponseMixin(object):
...
@@ -321,7 +330,7 @@ class ResponseMixin(object):
qvalue
=
Decimal
(
'1.0'
)
qvalue
=
Decimal
(
'1.0'
)
if
len
(
components
)
>
1
:
if
len
(
components
)
>
1
:
# Parse items that have a qvalue eg
text/html;q=0.9
# Parse items that have a qvalue eg
'text/html; q=0.9'
try
:
try
:
(
q
,
num
)
=
components
[
-
1
]
.
split
(
'='
)
(
q
,
num
)
=
components
[
-
1
]
.
split
(
'='
)
if
q
==
'q'
:
if
q
==
'q'
:
...
@@ -356,10 +365,10 @@ class ResponseMixin(object):
...
@@ -356,10 +365,10 @@ class ResponseMixin(object):
raise
ErrorResponse
(
status
.
HTTP_406_NOT_ACCEPTABLE
,
raise
ErrorResponse
(
status
.
HTTP_406_NOT_ACCEPTABLE
,
{
'detail'
:
'Could not satisfy the client
\'
s Accept header'
,
{
'detail'
:
'Could not satisfy the client
\'
s Accept header'
,
'available_types'
:
self
.
render
t
ed_media_types
})
'available_types'
:
self
.
rendered_media_types
})
@property
@property
def
render
t
ed_media_types
(
self
):
def
rendered_media_types
(
self
):
"""
"""
Return an list of all the media types that this resource can render.
Return an list of all the media types that this resource can render.
"""
"""
...
...
djangorestframework/renderers.py
View file @
4d126796
...
@@ -24,6 +24,7 @@ from urllib import quote_plus
...
@@ -24,6 +24,7 @@ from urllib import quote_plus
__all__
=
(
__all__
=
(
'BaseRenderer'
,
'BaseRenderer'
,
'TemplateRenderer'
,
'JSONRenderer'
,
'JSONRenderer'
,
'DocumentingHTMLRenderer'
,
'DocumentingHTMLRenderer'
,
'DocumentingXHTMLRenderer'
,
'DocumentingXHTMLRenderer'
,
...
@@ -87,10 +88,12 @@ class DocumentingTemplateRenderer(BaseRenderer):
...
@@ -87,10 +88,12 @@ class DocumentingTemplateRenderer(BaseRenderer):
template
=
None
template
=
None
def
_get_content
(
self
,
view
,
request
,
obj
,
media_type
):
def
_get_content
(
self
,
view
,
request
,
obj
,
media_type
):
"""Get the content as if it had been rendered by a non-documenting renderer.
"""
Get the content as if it had been rendered by a non-documenting renderer.
(Typically this will be the content as it would have been if the Resource had been
(Typically this will be the content as it would have been if the Resource had been
requested with an 'Accept: */*' header, although with verbose style formatting if appropriate.)"""
requested with an 'Accept: */*' header, although with verbose style formatting if appropriate.)
"""
# Find the first valid renderer and render the content. (Don't use another documenting renderer.)
# Find the first valid renderer and render the content. (Don't use another documenting renderer.)
renderers
=
[
renderer
for
renderer
in
view
.
renderers
if
not
isinstance
(
renderer
,
DocumentingTemplateRenderer
)]
renderers
=
[
renderer
for
renderer
in
view
.
renderers
if
not
isinstance
(
renderer
,
DocumentingTemplateRenderer
)]
...
@@ -103,12 +106,14 @@ class DocumentingTemplateRenderer(BaseRenderer):
...
@@ -103,12 +106,14 @@ class DocumentingTemplateRenderer(BaseRenderer):
return
'[
%
d bytes of binary content]'
return
'[
%
d bytes of binary content]'
return
content
return
content
def
_get_form_instance
(
self
,
view
):
def
_get_form_instance
(
self
,
view
):
"""Get a form, possibly bound to either the input or output data.
"""
Get a form, possibly bound to either the input or output data.
In the absence on of the Resource having an associated form then
In the absence on of the Resource having an associated form then
provide a form that can be used to submit arbitrary content."""
provide a form that can be used to submit arbitrary content.
"""
# Get the form instance if we have one bound to the input
# Get the form instance if we have one bound to the input
form_instance
=
getattr
(
view
,
'bound_form_instance'
,
None
)
form_instance
=
getattr
(
view
,
'bound_form_instance'
,
None
)
...
@@ -138,8 +143,10 @@ class DocumentingTemplateRenderer(BaseRenderer):
...
@@ -138,8 +143,10 @@ class DocumentingTemplateRenderer(BaseRenderer):
def
_get_generic_content_form
(
self
,
view
):
def
_get_generic_content_form
(
self
,
view
):
"""Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms
"""
(Which are typically application/x-www-form-urlencoded)"""
Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms
(Which are typically application/x-www-form-urlencoded)
"""
# If we're not using content overloading there's no point in supplying a generic form,
# If we're not using content overloading there's no point in supplying a generic form,
# as the view won't treat the form's value as the content of the request.
# as the view won't treat the form's value as the content of the request.
...
@@ -197,8 +204,8 @@ class DocumentingTemplateRenderer(BaseRenderer):
...
@@ -197,8 +204,8 @@ class DocumentingTemplateRenderer(BaseRenderer):
template
=
loader
.
get_template
(
self
.
template
)
template
=
loader
.
get_template
(
self
.
template
)
context
=
RequestContext
(
self
.
view
.
request
,
{
context
=
RequestContext
(
self
.
view
.
request
,
{
'content'
:
content
,
'content'
:
content
,
'resource'
:
self
.
view
,
'resource'
:
self
.
view
,
# TODO: rename to view
'request'
:
self
.
view
.
request
,
'request'
:
self
.
view
.
request
,
# TODO: remove
'response'
:
self
.
view
.
response
,
'response'
:
self
.
view
.
response
,
'description'
:
description
,
'description'
:
description
,
'name'
:
name
,
'name'
:
name
,
...
...
djangorestframework/resource.py
View file @
4d126796
from
django.db
.models
import
Model
from
django.db
import
models
from
django.db.models.query
import
QuerySet
from
django.db.models.query
import
QuerySet
from
django.db.models.fields.related
import
RelatedField
from
django.db.models.fields.related
import
RelatedField
from
django.utils.encoding
import
smart_unicode
import
decimal
import
decimal
import
inspect
import
inspect
import
re
import
re
def
_model_to_dict
(
instance
,
fields
=
None
,
exclude
=
None
):
"""
This is a clone of Django's ``django.forms.model_to_dict`` except that it
doesn't coerce related objects into primary keys.
"""
opts
=
instance
.
_meta
data
=
{}
for
f
in
opts
.
fields
+
opts
.
many_to_many
:
if
not
f
.
editable
:
continue
if
fields
and
not
f
.
name
in
fields
:
continue
if
exclude
and
f
.
name
in
exclude
:
continue
if
isinstance
(
f
,
models
.
ForeignKey
):
data
[
f
.
name
]
=
getattr
(
instance
,
f
.
name
)
else
:
data
[
f
.
name
]
=
f
.
value_from_object
(
instance
)
return
data
def
_object_to_data
(
obj
):
"""
Convert an object into a serializable representation.
"""
if
isinstance
(
obj
,
dict
):
# dictionaries
return
dict
([
(
key
,
_object_to_data
(
val
))
for
key
,
val
in
obj
.
iteritems
()
])
if
isinstance
(
obj
,
(
tuple
,
list
,
set
,
QuerySet
)):
# basic iterables
return
[
_object_to_data
(
item
)
for
item
in
obj
]
if
isinstance
(
obj
,
models
.
Manager
):
# Manager objects
ret
=
[
_object_to_data
(
item
)
for
item
in
obj
.
all
()]
if
isinstance
(
obj
,
models
.
Model
):
# Model instances
return
_object_to_data
(
_model_to_dict
(
obj
))
if
isinstance
(
obj
,
decimal
.
Decimal
):
# Decimals (force to string representation)
return
str
(
obj
)
if
inspect
.
isfunction
(
obj
)
and
not
inspect
.
getargspec
(
obj
)[
0
]:
# function with no args
return
_object_to_data
(
obj
())
if
inspect
.
ismethod
(
obj
)
and
len
(
inspect
.
getargspec
(
obj
)[
0
])
==
1
:
# method with only a 'self' args
return
_object_to_data
(
obj
())
# fallback
return
smart_unicode
(
obj
,
strings_only
=
True
)
# TODO: Replace this with new Serializer code based on Forms API.
# TODO: Replace this with new Serializer code based on Forms API.
#class Resource(object):
# def __init__(self, view):
# self.view = view
#
# def object_to_data(self, obj):
# pass
#
# def data_to_object(self, data, files):
# pass
#
#class FormResource(object):
# pass
#
#class ModelResource(object):
# pass
class
Resource
(
object
):
class
Resource
(
object
):
"""A Resource determines how an object maps to a serializable entity.
"""
Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets."""
A Resource determines how a python object maps to some serializable data.
Objects that a resource can act on include plain Python object instances, Django Models, and Django QuerySets.
"""
# The model attribute refers to the Django Model which this Resource maps to.
# The model attribute refers to the Django Model which this Resource maps to.
# (The Model's class, rather than an instance of the Model)
# (The Model's class, rather than an instance of the Model)
...
@@ -50,7 +123,7 @@ class Resource(object):
...
@@ -50,7 +123,7 @@ class Resource(object):
ret
=
thing
ret
=
thing
elif
isinstance
(
thing
,
decimal
.
Decimal
):
elif
isinstance
(
thing
,
decimal
.
Decimal
):
ret
=
str
(
thing
)
ret
=
str
(
thing
)
elif
isinstance
(
thing
,
Model
):
elif
isinstance
(
thing
,
models
.
Model
):
ret
=
_model
(
thing
,
fields
=
fields
)
ret
=
_model
(
thing
,
fields
=
fields
)
#elif isinstance(thing, HttpResponse): TRC
#elif isinstance(thing, HttpResponse): TRC
# raise HttpStatusCode(thing)
# raise HttpStatusCode(thing)
...
...
djangorestframework/response.py
View file @
4d126796
from
django.core.handlers.wsgi
import
STATUS_CODE_TEXT
from
django.core.handlers.wsgi
import
STATUS_CODE_TEXT
__all__
=
[
'Response'
,
'ErrorResponse'
]
__all__
=
(
'Response'
,
'ErrorResponse'
)
# TODO: remove raw_content/cleaned_content and just use content?
# TODO: remove raw_content/cleaned_content and just use content?
class
Response
(
object
):
class
Response
(
object
):
"""An HttpResponse that may include content that hasn't yet been serialized."""
"""
An HttpResponse that may include content that hasn't yet been serialized.
"""
def
__init__
(
self
,
status
=
200
,
content
=
None
,
headers
=
{}):
def
__init__
(
self
,
status
=
200
,
content
=
None
,
headers
=
{}):
self
.
status
=
status
self
.
status
=
status
self
.
has_content_body
=
content
is
not
None
self
.
has_content_body
=
content
is
not
None
...
@@ -15,12 +18,18 @@ class Response(object):
...
@@ -15,12 +18,18 @@ class Response(object):
@property
@property
def
status_text
(
self
):
def
status_text
(
self
):
"""Return reason text corresponding to our HTTP response status code.
"""
Provided for convenience."""
Return reason text corresponding to our HTTP response status code.
Provided for convenience.
"""
return
STATUS_CODE_TEXT
.
get
(
self
.
status
,
''
)
return
STATUS_CODE_TEXT
.
get
(
self
.
status
,
''
)
class
ErrorResponse
(
BaseException
):
class
ErrorResponse
(
BaseException
):
"""An exception representing an HttpResponse that should be returned immediately."""
"""
An exception representing an Response that should be returned immediately.
Any content should be serialized as-is, without being filtered.
"""
def
__init__
(
self
,
status
,
content
=
None
,
headers
=
{}):
def
__init__
(
self
,
status
,
content
=
None
,
headers
=
{}):
self
.
response
=
Response
(
status
,
content
=
content
,
headers
=
headers
)
self
.
response
=
Response
(
status
,
content
=
content
,
headers
=
headers
)
djangorestframework/status.py
View file @
4d126796
"""Descriptive HTTP status codes, for code readability.
"""
Descriptive HTTP status codes, for code readability.
See RFC 2616 - Sec 10: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
See RFC 2616 - Sec 10: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
Also, django.core.handlers.wsgi.STATUS_CODE_TEXT"""
Also see django.core.handlers.wsgi.STATUS_CODE_TEXT
"""
# Verbose format
# Verbose format
HTTP_100_CONTINUE
=
100
HTTP_100_CONTINUE
=
100
...
...
djangorestframework/templates/renderer.html
View file @
4d126796
...
@@ -48,7 +48,7 @@
...
@@ -48,7 +48,7 @@
<h2>
GET {{ name }}
</h2>
<h2>
GET {{ name }}
</h2>
<div
class=
'submit-row'
style=
'margin: 0; border: 0'
>
<div
class=
'submit-row'
style=
'margin: 0; border: 0'
>
<a
href=
'{{ request.path }}'
rel=
"nofollow"
style=
'float: left'
>
GET
</a>
<a
href=
'{{ request.path }}'
rel=
"nofollow"
style=
'float: left'
>
GET
</a>
{% for media_type in resource.render
t
ed_media_types %}
{% for media_type in resource.rendered_media_types %}
{% with resource.ACCEPT_QUERY_PARAM|add:"="|add:media_type as param %}
{% with resource.ACCEPT_QUERY_PARAM|add:"="|add:media_type as param %}
[
<a
href=
'{{ request.path|add_query_param:param }}'
rel=
"nofollow"
>
{{ media_type }}
</a>
]
[
<a
href=
'{{ request.path|add_query_param:param }}'
rel=
"nofollow"
>
{{ media_type }}
</a>
]
{% endwith %}
{% endwith %}
...
...
djangorestframework/tests/resource.py
0 → 100644
View file @
4d126796
"""Tests for the resource module"""
from
django.test
import
TestCase
from
djangorestframework.resource
import
_object_to_data
import
datetime
import
decimal
class
TestObjectToData
(
TestCase
):
"""Tests for the _object_to_data function"""
def
test_decimal
(
self
):
"""Decimals need to be converted to a string representation."""
self
.
assertEquals
(
_object_to_data
(
decimal
.
Decimal
(
'1.5'
)),
'1.5'
)
def
test_function
(
self
):
"""Functions with no arguments should be called."""
def
foo
():
return
1
self
.
assertEquals
(
_object_to_data
(
foo
),
1
)
def
test_method
(
self
):
"""Methods with only a ``self`` argument should be called."""
class
Foo
(
object
):
def
foo
(
self
):
return
1
self
.
assertEquals
(
_object_to_data
(
Foo
()
.
foo
),
1
)
def
test_datetime
(
self
):
"""datetime objects are left as-is."""
now
=
datetime
.
datetime
.
now
()
self
.
assertEquals
(
_object_to_data
(
now
),
now
)
\ No newline at end of file
djangorestframework/validators.py
View file @
4d126796
...
@@ -31,20 +31,24 @@ class FormValidator(BaseValidator):
...
@@ -31,20 +31,24 @@ class FormValidator(BaseValidator):
def
validate
(
self
,
content
):
def
validate
(
self
,
content
):
"""Given some content as input return some cleaned, validated content.
"""
Given some content as input return some cleaned, validated content.
Raises a ErrorResponse with status code 400 (Bad Request) on failure.
Raises a ErrorResponse with status code 400 (Bad Request) on failure.
Validation is standard form validation, with an additional constraint that no extra unknown fields may be supplied.
Validation is standard form validation, with an additional constraint that no extra unknown fields may be supplied.
On failure the ErrorResponse content is a dict which may contain 'errors' and 'field-errors' keys.
On failure the ErrorResponse content is a dict which may contain 'errors' and 'field-errors' keys.
If the 'errors' key exists it is a list of strings of non-field errors.
If the 'errors' key exists it is a list of strings of non-field errors.
If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}."""
If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}.
"""
return
self
.
_validate
(
content
)
return
self
.
_validate
(
content
)
def
_validate
(
self
,
content
,
allowed_extra_fields
=
()):
def
_validate
(
self
,
content
,
allowed_extra_fields
=
()):
"""Wrapped by validate to hide the extra_fields option that the ModelValidatorMixin uses.
"""
Wrapped by validate to hide the extra_fields option that the ModelValidatorMixin uses.
extra_fields is a list of fields which are not defined by the form, but which we still
extra_fields is a list of fields which are not defined by the form, but which we still
expect to see on the input."""
expect to see on the input.
"""
bound_form
=
self
.
get_bound_form
(
content
)
bound_form
=
self
.
get_bound_form
(
content
)
if
bound_form
is
None
:
if
bound_form
is
None
:
...
@@ -138,7 +142,8 @@ class ModelFormValidator(FormValidator):
...
@@ -138,7 +142,8 @@ class ModelFormValidator(FormValidator):
# TODO: test the different validation here to allow for get get_absolute_url to be supplied on input and not bork out
# TODO: test the different validation here to allow for get get_absolute_url to be supplied on input and not bork out
# TODO: be really strict on fields - check they match in the handler methods. (this isn't a validator thing tho.)
# TODO: be really strict on fields - check they match in the handler methods. (this isn't a validator thing tho.)
def
validate
(
self
,
content
):
def
validate
(
self
,
content
):
"""Given some content as input return some cleaned, validated content.
"""
Given some content as input return some cleaned, validated content.
Raises a ErrorResponse with status code 400 (Bad Request) on failure.
Raises a ErrorResponse with status code 400 (Bad Request) on failure.
Validation is standard form or model form validation,
Validation is standard form or model form validation,
...
@@ -148,7 +153,8 @@ class ModelFormValidator(FormValidator):
...
@@ -148,7 +153,8 @@ class ModelFormValidator(FormValidator):
On failure the ErrorResponse content is a dict which may contain 'errors' and 'field-errors' keys.
On failure the ErrorResponse content is a dict which may contain 'errors' and 'field-errors' keys.
If the 'errors' key exists it is a list of strings of non-field errors.
If the 'errors' key exists it is a list of strings of non-field errors.
If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}."""
If the 'field-errors' key exists it is a dict of {field name as string: list of errors as strings}.
"""
return
self
.
_validate
(
content
,
allowed_extra_fields
=
self
.
_property_fields_set
)
return
self
.
_validate
(
content
,
allowed_extra_fields
=
self
.
_property_fields_set
)
...
...
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