Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
ecommerce
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
ecommerce
Commits
ac4a43a1
Commit
ac4a43a1
authored
Feb 03, 2017
by
Saleem Latif
Committed by
Saleem Latif
Feb 28, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update coupon redemption page to account for the catalog id
parent
da2db893
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
221 additions
and
9 deletions
+221
-9
AUTHORS
+1
-0
ecommerce/coupons/tests/mixins.py
+35
-0
ecommerce/coupons/tests/test_views.py
+4
-4
ecommerce/coupons/utils.py
+59
-0
ecommerce/extensions/api/v2/tests/views/test_vouchers.py
+112
-0
ecommerce/extensions/api/v2/views/vouchers.py
+8
-3
ecommerce/extensions/offer/models.py
+1
-1
ecommerce/extensions/voucher/utils.py
+1
-1
No files found.
AUTHORS
View file @
ac4a43a1
...
@@ -16,3 +16,4 @@ Vedran Karačić <vedran@edx.org>
...
@@ -16,3 +16,4 @@ Vedran Karačić <vedran@edx.org>
Awais Jibran <awaisdar001@gmail.com>
Awais Jibran <awaisdar001@gmail.com>
Bill DeRusha <bill@edx.org>
Bill DeRusha <bill@edx.org>
Ivan Ivić <iivic@edx.org>
Ivan Ivić <iivic@edx.org>
Saleem Latif <saleem_ee@hotmail.com>
ecommerce/coupons/tests/mixins.py
View file @
ac4a43a1
...
@@ -52,6 +52,41 @@ class CourseCatalogMockMixin(object):
...
@@ -52,6 +52,41 @@ class CourseCatalogMockMixin(object):
content_type
=
'application/json'
content_type
=
'application/json'
)
)
def
mock_fetch_course_catalog
(
self
,
catalog_id
=
1
,
expected_query
=
"*:*"
,
expected_status
=
'200'
):
"""
Helper function to register a catalog API endpoint for fetching catalog by catalog id.
"""
course_catalog
=
{
"id"
:
1
,
"name"
:
"All Courses"
,
"query"
:
expected_query
,
"courses_count"
:
1
,
"viewers"
:
[]
}
course_run_info_json
=
json
.
dumps
(
course_catalog
)
course_run_url
=
'{}catalogs/{}/'
.
format
(
settings
.
COURSE_CATALOG_API_URL
,
catalog_id
,
)
httpretty
.
register_uri
(
httpretty
.
GET
,
course_run_url
,
body
=
course_run_info_json
,
content_type
=
'application/json'
,
status
=
expected_status
,
)
def
mock_course_catalog_api_for_catalog_voucher
(
self
,
catalog_id
=
1
,
query
=
"*:*"
,
expected_status
=
'200'
,
course_run
=
None
,
):
"""
Helper function to register course catalog API endpoint for fetching course run information and
catalog by catalog id.
"""
self
.
mock_fetch_course_catalog
(
catalog_id
=
catalog_id
,
expected_query
=
query
,
expected_status
=
expected_status
)
self
.
mock_dynamic_catalog_course_runs_api
(
query
=
query
,
course_run
=
course_run
)
def
mock_dynamic_catalog_course_runs_api
(
self
,
course_run
=
None
,
partner_code
=
None
,
query
=
None
,
def
mock_dynamic_catalog_course_runs_api
(
self
,
course_run
=
None
,
partner_code
=
None
,
query
=
None
,
course_run_info
=
None
):
course_run_info
=
None
):
"""
"""
...
...
ecommerce/coupons/tests/test_views.py
View file @
ac4a43a1
...
@@ -87,7 +87,7 @@ class GetVoucherTests(CourseCatalogTestMixin, TestCase):
...
@@ -87,7 +87,7 @@ class GetVoucherTests(CourseCatalogTestMixin, TestCase):
def
test_no_product
(
self
):
def
test_no_product
(
self
):
""" Verify that an exception is raised if there is no product. """
""" Verify that an exception is raised if there is no product. """
code
=
FuzzyText
()
.
fuzz
()
.
upper
()
code
=
FuzzyText
()
.
fuzz
()
voucher
=
VoucherFactory
(
code
=
code
)
voucher
=
VoucherFactory
(
code
=
code
)
offer
=
ConditionalOfferFactory
()
offer
=
ConditionalOfferFactory
()
voucher
.
offers
.
add
(
offer
)
voucher
.
offers
.
add
(
offer
)
...
@@ -230,7 +230,7 @@ class CouponOfferViewTests(ApiMockMixin, CouponMixin, CourseCatalogTestMixin, Lm
...
@@ -230,7 +230,7 @@ class CouponOfferViewTests(ApiMockMixin, CouponMixin, CourseCatalogTestMixin, Lm
def
test_no_product
(
self
):
def
test_no_product
(
self
):
""" Verify an error is returned for voucher with no product. """
""" Verify an error is returned for voucher with no product. """
code
=
FuzzyText
()
.
fuzz
()
.
upper
()
code
=
FuzzyText
()
.
fuzz
()
no_product_range
=
RangeFactory
()
no_product_range
=
RangeFactory
()
prepare_voucher
(
code
=
code
,
_range
=
no_product_range
)
prepare_voucher
(
code
=
code
,
_range
=
no_product_range
)
url
=
self
.
path
+
'?code={}'
.
format
(
code
)
url
=
self
.
path
+
'?code={}'
.
format
(
code
)
...
@@ -348,7 +348,7 @@ class CouponRedeemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin
...
@@ -348,7 +348,7 @@ class CouponRedeemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin
def
test_invalid_voucher_code
(
self
):
def
test_invalid_voucher_code
(
self
):
""" Verify an error is returned when voucher does not exist. """
""" Verify an error is returned when voucher does not exist. """
code
=
FuzzyText
()
.
fuzz
()
.
upper
()
code
=
FuzzyText
()
.
fuzz
()
url
=
self
.
redeem_url
+
'?code={}&sku={}'
.
format
(
code
,
self
.
stock_record
.
partner_sku
)
url
=
self
.
redeem_url
+
'?code={}&sku={}'
.
format
(
code
,
self
.
stock_record
.
partner_sku
)
response
=
self
.
client
.
get
(
url
)
response
=
self
.
client
.
get
(
url
)
msg
=
'No voucher found with code {code}'
.
format
(
code
=
code
)
msg
=
'No voucher found with code {code}'
.
format
(
code
=
code
)
...
@@ -365,7 +365,7 @@ class CouponRedeemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin
...
@@ -365,7 +365,7 @@ class CouponRedeemViewTests(CouponMixin, CourseCatalogTestMixin, LmsApiMockMixin
""" Verify an error is returned for expired coupon. """
""" Verify an error is returned for expired coupon. """
start_datetime
=
now
()
-
datetime
.
timedelta
(
days
=
20
)
start_datetime
=
now
()
-
datetime
.
timedelta
(
days
=
20
)
end_datetime
=
now
()
-
datetime
.
timedelta
(
days
=
10
)
end_datetime
=
now
()
-
datetime
.
timedelta
(
days
=
10
)
code
=
FuzzyText
()
.
fuzz
()
.
upper
()
code
=
FuzzyText
()
.
fuzz
()
__
,
product
=
prepare_voucher
(
code
=
code
,
start_datetime
=
start_datetime
,
end_datetime
=
end_datetime
)
__
,
product
=
prepare_voucher
(
code
=
code
,
start_datetime
=
start_datetime
,
end_datetime
=
end_datetime
)
url
=
self
.
redeem_url
+
'?code={}&sku={}'
.
format
(
code
,
StockRecord
.
objects
.
get
(
product
=
product
)
.
partner_sku
)
url
=
self
.
redeem_url
+
'?code={}&sku={}'
.
format
(
code
,
StockRecord
.
objects
.
get
(
product
=
product
)
.
partner_sku
)
response
=
self
.
client
.
get
(
url
)
response
=
self
.
client
.
get
(
url
)
...
...
ecommerce/coupons/utils.py
View file @
ac4a43a1
""" Coupon related utility functions. """
""" Coupon related utility functions. """
import
logging
import
hashlib
import
hashlib
from
django.conf
import
settings
from
django.conf
import
settings
from
django.core.cache
import
cache
from
django.core.cache
import
cache
from
oscar.core.loading
import
get_model
from
oscar.core.loading
import
get_model
from
slumber.exceptions
import
HttpNotFoundError
from
ecommerce.courses.utils
import
traverse_pagination
from
ecommerce.courses.utils
import
traverse_pagination
from
ecommerce.core.utils
import
get_cache_key
Product
=
get_model
(
'catalogue'
,
'Product'
)
Product
=
get_model
(
'catalogue'
,
'Product'
)
logger
=
logging
.
getLogger
(
__name__
)
def
get_catalog_course_runs
(
site
,
query
,
limit
=
None
,
offset
=
None
):
def
get_catalog_course_runs
(
site
,
query
,
limit
=
None
,
offset
=
None
):
"""
"""
...
@@ -106,3 +111,57 @@ def prepare_course_seat_types(course_seat_types):
...
@@ -106,3 +111,57 @@ def prepare_course_seat_types(course_seat_types):
if
course_seat_types
:
if
course_seat_types
:
return
','
.
join
(
seat_type
.
lower
()
for
seat_type
in
course_seat_types
)
return
','
.
join
(
seat_type
.
lower
()
for
seat_type
in
course_seat_types
)
return
None
return
None
def
fetch_course_catalog
(
site
,
catalog_id
):
"""
Fetch course catalog for the given catalog id.
This method will fetch catalog for given catalog id, if there is no catalog with the given
catalog id, method will return `None`.
Arguments:
site (Site): Instance of the current site.
catalog_id (int): An integer specifying the primary key value of catalog to fetch.
Example:
>>> fetch_course_catalog(site, catalog_id=1)
{
"id": 1,
"name": "All Courses",
"query": "*:*",
...
}
Returns:
(dict): A dictionary containing key/value pairs corresponding to catalog attribute/values.
Raises:
ConnectionError: requests exception "ConnectionError", raised if if ecommerce is unable to connect
to enterprise api server.
SlumberBaseException: base slumber exception "SlumberBaseException", raised if API response contains
http error status like 4xx, 5xx etc.
Timeout: requests exception "Timeout", raised if enterprise API is taking too long for returning
a response. This exception is raised for both connection timeout and read timeout.
"""
api_resource
=
'catalogs'
cache_key
=
get_cache_key
(
site_domain
=
site
.
domain
,
resource
=
api_resource
,
catalog_id
=
catalog_id
,
)
response
=
cache
.
get
(
cache_key
)
if
not
response
:
api
=
site
.
siteconfiguration
.
course_catalog_api_client
endpoint
=
getattr
(
api
,
api_resource
)
try
:
response
=
endpoint
(
catalog_id
)
.
get
()
except
HttpNotFoundError
:
logger
.
exception
(
"Catalog '
%
s' not found."
,
catalog_id
)
raise
cache
.
set
(
cache_key
,
response
,
settings
.
COURSES_API_CACHE_TIMEOUT
)
return
response
ecommerce/extensions/api/v2/tests/views/test_vouchers.py
View file @
ac4a43a1
...
@@ -13,6 +13,7 @@ from opaque_keys.edx.keys import CourseKey
...
@@ -13,6 +13,7 @@ from opaque_keys.edx.keys import CourseKey
from
oscar.core.loading
import
get_model
from
oscar.core.loading
import
get_model
from
oscar.test.factories
import
BenefitFactory
,
OrderLineFactory
,
OrderFactory
,
RangeFactory
from
oscar.test.factories
import
BenefitFactory
,
OrderLineFactory
,
OrderFactory
,
RangeFactory
from
requests.exceptions
import
ConnectionError
,
Timeout
from
requests.exceptions
import
ConnectionError
,
Timeout
from
rest_framework
import
status
from
rest_framework.test
import
APIRequestFactory
from
rest_framework.test
import
APIRequestFactory
from
slumber.exceptions
import
SlumberBaseException
from
slumber.exceptions
import
SlumberBaseException
...
@@ -407,6 +408,47 @@ class VoucherViewOffersEndpointTests(CourseCatalogMockMixin, CouponMixin, Course
...
@@ -407,6 +408,47 @@ class VoucherViewOffersEndpointTests(CourseCatalogMockMixin, CouponMixin, Course
'voucher_end_date'
:
voucher
.
end_datetime
,
'voucher_end_date'
:
voucher
.
end_datetime
,
})
})
@mock_course_catalog_api_client
def
test_get_offers_for_course_catalog_voucher
(
self
):
""" Verify that the course offers data is returned for a course catalog voucher. """
catalog_id
=
1
catalog_query
=
'*:*'
# Populate database for the test case.
course
,
seat
=
self
.
create_course_and_seat
()
new_range
,
__
=
Range
.
objects
.
get_or_create
(
course_catalog
=
catalog_id
,
course_seat_types
=
'verified'
)
new_range
.
add_product
(
seat
)
voucher
,
__
=
prepare_voucher
(
_range
=
new_range
,
benefit_value
=
10
)
# Mock network calls
self
.
mock_dynamic_catalog_course_runs_api
(
query
=
catalog_query
,
course_run
=
course
)
self
.
mock_fetch_course_catalog
(
catalog_id
=
catalog_id
,
expected_query
=
catalog_query
)
benefit
=
voucher
.
offers
.
first
()
.
benefit
request
=
self
.
prepare_offers_listing_request
(
voucher
.
code
)
offers
=
VoucherViewSet
()
.
get_offers
(
request
=
request
,
voucher
=
voucher
)[
'results'
]
first_offer
=
offers
[
0
]
# Verify that offers are returned when voucher is created using course catalog
self
.
assertEqual
(
len
(
offers
),
1
)
self
.
assertDictEqual
(
first_offer
,
{
'benefit'
:
{
'type'
:
benefit
.
type
,
'value'
:
benefit
.
value
},
'contains_verified'
:
True
,
'course_start_date'
:
'2016-05-01T00:00:00Z'
,
'id'
:
course
.
id
,
'image_url'
:
'path/to/the/course/image'
,
'multiple_credit_providers'
:
False
,
'organization'
:
CourseKey
.
from_string
(
course
.
id
)
.
org
,
'credit_provider_price'
:
None
,
'seat_type'
:
course
.
type
,
'stockrecords'
:
serializers
.
StockRecordSerializer
(
seat
.
stockrecords
.
first
())
.
data
,
'title'
:
course
.
name
,
'voucher_end_date'
:
voucher
.
end_datetime
,
})
def
test_get_course_offer_data
(
self
):
def
test_get_course_offer_data
(
self
):
""" Verify that the course offers data is properly formatted. """
""" Verify that the course offers data is properly formatted. """
benefit
=
BenefitFactory
()
benefit
=
BenefitFactory
()
...
@@ -474,3 +516,73 @@ class VoucherViewOffersEndpointTests(CourseCatalogMockMixin, CouponMixin, Course
...
@@ -474,3 +516,73 @@ class VoucherViewOffersEndpointTests(CourseCatalogMockMixin, CouponMixin, Course
self
.
assertEqual
(
offer
[
'image_url'
],
''
)
self
.
assertEqual
(
offer
[
'image_url'
],
''
)
self
.
assertEqual
(
offer
[
'course_start_date'
],
None
)
self
.
assertEqual
(
offer
[
'course_start_date'
],
None
)
@mock_course_catalog_api_client
def
test_offers_api_endpoint_for_course_catalog_voucher
(
self
):
"""
Verify that the course offers data is returned for a course catalog voucher.
"""
catalog_id
=
1
catalog_query
=
'*:*'
# Populate database for the test case.
course
,
seat
=
self
.
create_course_and_seat
()
new_range
,
__
=
Range
.
objects
.
get_or_create
(
course_catalog
=
catalog_id
,
course_seat_types
=
'verified'
)
new_range
.
add_product
(
seat
)
voucher
,
__
=
prepare_voucher
(
_range
=
new_range
,
benefit_value
=
10
)
# Mock network calls
self
.
mock_course_catalog_api_for_catalog_voucher
(
catalog_id
=
catalog_id
,
query
=
catalog_query
,
course_run
=
course
)
benefit
=
voucher
.
offers
.
first
()
.
benefit
request
=
self
.
prepare_offers_listing_request
(
voucher
.
code
)
response
=
self
.
endpointView
(
request
)
# Verify that offers are returned when voucher is created using course catalog
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertListEqual
(
response
.
data
[
'results'
],
[{
'benefit'
:
{
'type'
:
benefit
.
type
,
'value'
:
benefit
.
value
},
'contains_verified'
:
True
,
'course_start_date'
:
'2016-05-01T00:00:00Z'
,
'id'
:
course
.
id
,
'image_url'
:
'path/to/the/course/image'
,
'multiple_credit_providers'
:
False
,
'organization'
:
CourseKey
.
from_string
(
course
.
id
)
.
org
,
'credit_provider_price'
:
None
,
'seat_type'
:
course
.
type
,
'stockrecords'
:
serializers
.
StockRecordSerializer
(
seat
.
stockrecords
.
first
())
.
data
,
'title'
:
course
.
name
,
'voucher_end_date'
:
voucher
.
end_datetime
,
}],
)
@mock_course_catalog_api_client
def
test_get_offers_for_course_catalog_voucher_api_error
(
self
):
"""
Verify that offers api endpoint returns proper message if course catalog api returns error.
"""
catalog_id
=
1
catalog_query
=
'*:*'
# Populate database for the test case.
course
,
seat
=
self
.
create_course_and_seat
()
new_range
,
__
=
Range
.
objects
.
get_or_create
(
course_catalog
=
catalog_id
,
course_seat_types
=
'verified'
)
new_range
.
add_product
(
seat
)
voucher
,
__
=
prepare_voucher
(
_range
=
new_range
,
benefit_value
=
10
)
# Mock network calls
self
.
mock_course_catalog_api_for_catalog_voucher
(
catalog_id
=
catalog_id
,
query
=
catalog_query
,
expected_status
=
status
.
HTTP_404_NOT_FOUND
,
course_run
=
course
)
request
=
self
.
prepare_offers_listing_request
(
voucher
.
code
)
response
=
self
.
endpointView
(
request
)
# Verify that offers are returned when voucher is created using course catalog
self
.
assertEqual
(
response
.
status_code
,
status
.
HTTP_400_BAD_REQUEST
)
ecommerce/extensions/api/v2/views/vouchers.py
View file @
ac4a43a1
...
@@ -15,9 +15,9 @@ from rest_framework.response import Response
...
@@ -15,9 +15,9 @@ from rest_framework.response import Response
from
slumber.exceptions
import
SlumberBaseException
from
slumber.exceptions
import
SlumberBaseException
from
ecommerce.core.constants
import
DEFAULT_CATALOG_PAGE_SIZE
from
ecommerce.core.constants
import
DEFAULT_CATALOG_PAGE_SIZE
from
ecommerce.coupons.utils
import
get_catalog_course_runs
from
ecommerce.courses.models
import
Course
from
ecommerce.courses.models
import
Course
from
ecommerce.courses.utils
import
get_course_info_from_catalog
from
ecommerce.courses.utils
import
get_course_info_from_catalog
from
ecommerce.coupons.utils
import
get_catalog_course_runs
,
fetch_course_catalog
from
ecommerce.extensions.api
import
serializers
from
ecommerce.extensions.api
import
serializers
from
ecommerce.extensions.api.permissions
import
IsOffersOrIsAuthenticatedAndStaff
from
ecommerce.extensions.api.permissions
import
IsOffersOrIsAuthenticatedAndStaff
from
ecommerce.extensions.api.v2.views
import
NonDestroyableModelViewSet
from
ecommerce.extensions.api.v2.views
import
NonDestroyableModelViewSet
...
@@ -74,10 +74,10 @@ class VoucherViewSet(NonDestroyableModelViewSet):
...
@@ -74,10 +74,10 @@ class VoucherViewSet(NonDestroyableModelViewSet):
try
:
try
:
offers_data
=
self
.
get_offers
(
request
,
voucher
)
offers_data
=
self
.
get_offers
(
request
,
voucher
)
except
(
ConnectionError
,
SlumberBaseException
,
Timeout
):
except
(
ConnectionError
,
SlumberBaseException
,
Timeout
):
logger
.
error
(
'Could not
get course information
.'
)
logger
.
error
(
'Could not
connect to course catalog service
.'
)
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
)
return
Response
(
status
=
status
.
HTTP_400_BAD_REQUEST
)
except
Product
.
DoesNotExist
:
except
Product
.
DoesNotExist
:
logger
.
error
(
'Could not
get product information
for voucher with code
%
s.'
,
code
)
logger
.
error
(
'Could not
locate product
for voucher with code
%
s.'
,
code
)
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
next_page
=
offers_data
[
'next'
]
next_page
=
offers_data
[
'next'
]
...
@@ -214,9 +214,14 @@ class VoucherViewSet(NonDestroyableModelViewSet):
...
@@ -214,9 +214,14 @@ class VoucherViewSet(NonDestroyableModelViewSet):
"""
"""
benefit
=
voucher
.
offers
.
first
()
.
benefit
benefit
=
voucher
.
offers
.
first
()
.
benefit
catalog_query
=
benefit
.
range
.
catalog_query
catalog_query
=
benefit
.
range
.
catalog_query
catalog_id
=
benefit
.
range
.
course_catalog
next_page
=
None
next_page
=
None
offers
=
[]
offers
=
[]
if
catalog_id
:
catalog
=
fetch_course_catalog
(
request
.
site
,
catalog_id
)
catalog_query
=
catalog
.
get
(
"query"
)
if
catalog
else
catalog_query
if
catalog_query
:
if
catalog_query
:
offers
,
next_page
=
self
.
get_offers_from_query
(
request
,
voucher
,
catalog_query
)
offers
,
next_page
=
self
.
get_offers_from_query
(
request
,
voucher
,
catalog_query
)
else
:
else
:
...
...
ecommerce/extensions/offer/models.py
View file @
ac4a43a1
...
@@ -297,7 +297,7 @@ class Range(AbstractRange):
...
@@ -297,7 +297,7 @@ class Range(AbstractRange):
return
len
(
self
.
all_products
())
return
len
(
self
.
all_products
())
def
all_products
(
self
):
def
all_products
(
self
):
if
self
.
catalog_query
and
self
.
course_seat_types
:
if
(
self
.
catalog_query
or
self
.
course_catalog
)
and
self
.
course_seat_types
:
# Backbone calls the Voucher Offers API endpoint which gets the products from the Course Catalog Service
# Backbone calls the Voucher Offers API endpoint which gets the products from the Course Catalog Service
return
[]
return
[]
if
self
.
catalog
:
if
self
.
catalog
:
...
...
ecommerce/extensions/voucher/utils.py
View file @
ac4a43a1
...
@@ -635,7 +635,7 @@ def get_voucher_and_products_from_code(code):
...
@@ -635,7 +635,7 @@ def get_voucher_and_products_from_code(code):
voucher_range
=
voucher
.
offers
.
first
()
.
benefit
.
range
voucher_range
=
voucher
.
offers
.
first
()
.
benefit
.
range
products
=
voucher_range
.
all_products
()
products
=
voucher_range
.
all_products
()
if
products
or
voucher_range
.
catalog_query
:
if
products
or
voucher_range
.
catalog_query
or
voucher_range
.
course_catalog
:
# List of products is empty in case of Multi-course coupon
# List of products is empty in case of Multi-course coupon
return
voucher
,
products
return
voucher
,
products
else
:
else
:
...
...
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