Commit b8f7a126 by asadiqbal Committed by zubair-arbi

UI changes on basket page.

parent 4f7b1395
...@@ -17,3 +17,4 @@ Awais Jibran <awaisdar001@gmail.com> ...@@ -17,3 +17,4 @@ 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> Saleem Latif <saleem_ee@hotmail.com>
Asad Iqbal <aiqbal@edx.org>
...@@ -8,7 +8,7 @@ msgid "" ...@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-22 10:47+0000\n" "POT-Creation-Date: 2017-12-15 09:43-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -42,7 +42,7 @@ msgid "A valid course ID is required" ...@@ -42,7 +42,7 @@ msgid "A valid course ID is required"
msgstr "" msgstr ""
#: ecommerce/static/js/models/coupon_model.js:106 #: ecommerce/static/js/models/coupon_model.js:106
#, c-format #, javascript-format
msgid "Email domain {%s} is invalid." msgid "Email domain {%s} is invalid."
msgstr "" msgstr ""
...@@ -253,30 +253,23 @@ msgstr "" ...@@ -253,30 +253,23 @@ msgstr ""
#: ecommerce/static/js/payment_processors/cybersource.js:249 #: ecommerce/static/js/payment_processors/cybersource.js:249
msgid "" msgid ""
"An error occurred while processing your payment. You have NOT been charged. " "An error occurred while processing your payment. You have NOT been charged. "
"Please try again, or select another payment method."
msgstr "" msgstr ""
#: ecommerce/static/js/payment_processors/stripe.js:71 #: ecommerce/static/js/payment_processors/stripe.js:71
msgid "" msgid ""
"An error occurred while attempting to process your payment. You have not " "An error occurred while attempting to process your payment. You have not "
"been " "been charged. Please check your payment details, and try again."
msgstr "" msgstr ""
#: ecommerce/static/js/payment_processors/stripe.js:110 #: ecommerce/static/js/payment_processors/stripe.js:110
msgid "An error occurred while processing your payment. " msgid "An error occurred while processing your payment. Please try again."
msgstr "" msgstr ""
#: ecommerce/static/js/utils/utils.js:184 #: ecommerce/static/js/utils/utils.js:184
msgid "Trailing comma not allowed." msgid "Trailing comma not allowed."
msgstr "" msgstr ""
#: ecommerce/static/js/utils/validation_patterns.js:18
msgid "The course ID is invalid."
msgstr ""
#: ecommerce/static/js/utils/validation_patterns.js:26
msgid "The product name cannot contain HTML."
msgstr ""
#: ecommerce/static/js/views/coupon_detail_view.js:107 #: ecommerce/static/js/views/coupon_detail_view.js:107
#: ecommerce/static/js/views/coupon_form_view.js:60 #: ecommerce/static/js/views/coupon_form_view.js:60
msgid "Can be used once by one customer" msgid "Can be used once by one customer"
...@@ -380,12 +373,15 @@ msgid "Professional Education" ...@@ -380,12 +373,15 @@ msgid "Professional Education"
msgstr "" msgstr ""
#: ecommerce/static/js/views/course_form_view.js:66 #: ecommerce/static/js/views/course_form_view.js:66
msgid "Paid certificate track with initial verification and Professional " msgid ""
"Paid certificate track with initial verification and Professional Education "
"Certificate"
msgstr "" msgstr ""
#: ecommerce/static/js/views/course_form_view.js:72 #: ecommerce/static/js/views/course_form_view.js:72
msgid "" msgid ""
"Paid certificate track with initial verification and Verified Certificate, " "Paid certificate track with initial verification and Verified Certificate, "
"and option to purchase credit"
msgstr "" msgstr ""
#. Translators: _START_, _END_, and _TOTAL_ are placeholders. Do NOT translate them. #. Translators: _START_, _END_, and _TOTAL_ are placeholders. Do NOT translate them.
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-22 10:47+0000\n" "POT-Creation-Date: 2017-12-15 09:43-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: ecommerce/static/js/models/coupon_model.js #: ecommerce/static/js/models/coupon_model.js
...@@ -46,7 +46,7 @@ msgid "A valid course ID is required" ...@@ -46,7 +46,7 @@ msgid "A valid course ID is required"
msgstr "À välïd çöürsé ÌD ïs réqüïréd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" msgstr "À välïd çöürsé ÌD ïs réqüïréd Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
#: ecommerce/static/js/models/coupon_model.js #: ecommerce/static/js/models/coupon_model.js
#, c-format #, javascript-format
msgid "Email domain {%s} is invalid." msgid "Email domain {%s} is invalid."
msgstr "Émäïl dömäïn {%s} ïs ïnvälïd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#" msgstr "Émäïl dömäïn {%s} ïs ïnvälïd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
...@@ -281,38 +281,35 @@ msgstr "" ...@@ -281,38 +281,35 @@ msgstr ""
#: ecommerce/static/js/payment_processors/cybersource.js #: ecommerce/static/js/payment_processors/cybersource.js
msgid "" msgid ""
"An error occurred while processing your payment. You have NOT been charged. " "An error occurred while processing your payment. You have NOT been charged. "
"Please try again, or select another payment method."
msgstr "" msgstr ""
"Àn érrör öççürréd whïlé pröçéssïng ýöür päýmént. Ýöü hävé NÖT ßéén çhärgéd." "Àn érrör öççürréd whïlé pröçéssïng ýöür päýmént. Ýöü hävé NÖT ßéén çhärgéd. "
" Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυ#" "Pléäsé trý ägäïn, ör séléçt änöthér päýmént méthöd. Ⱡ'σяє#"
#: ecommerce/static/js/payment_processors/stripe.js #: ecommerce/static/js/payment_processors/stripe.js
msgid "" msgid ""
"An error occurred while attempting to process your payment. You have not " "An error occurred while attempting to process your payment. You have not "
"been " "been charged. Please check your payment details, and try again."
msgstr "" msgstr ""
"Àn érrör öççürréd whïlé ättémptïng tö pröçéss ýöür päýmént. Ýöü hävé nöt " "Àn érrör öççürréd whïlé ättémptïng tö pröçéss ýöür päýmént. Ýöü hävé nöt "
"ßéén Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєт#" "ßéén çhärgéd. Pléäsé çhéçk ýöür päýmént détäïls, änd trý ägäïn. Ⱡ'σяєм ιρѕυм"
" ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ єιυѕмσ∂ тємρσя "
"ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм νєηιαм, qυιѕ "
"ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα ¢σммσ∂σ "
"¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє νєłιт єѕѕє "
"¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢¢αє¢αт ¢υρι∂αтαт "
"ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢ια ∂єѕєяυηт мσłłιт αηιм ι∂ єѕт łαвσя#"
#: ecommerce/static/js/payment_processors/stripe.js #: ecommerce/static/js/payment_processors/stripe.js
msgid "An error occurred while processing your payment. " msgid "An error occurred while processing your payment. Please try again."
msgstr "" msgstr ""
"Àn érrör öççürréd whïlé pröçéssïng ýöür päýmént. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт " "Àn érrör öççürréd whïlé pröçéssïng ýöür päýmént. Pléäsé trý ägäïn. Ⱡ'σяєм "
"αмєт, ¢σηѕє¢тєтυя α#" "ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #"
#: ecommerce/static/js/utils/utils.js #: ecommerce/static/js/utils/utils.js
msgid "Trailing comma not allowed." msgid "Trailing comma not allowed."
msgstr "Träïlïng çömmä nöt ällöwéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#" msgstr "Träïlïng çömmä nöt ällöwéd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#"
#: ecommerce/static/js/utils/validation_patterns.js
msgid "The course ID is invalid."
msgstr "Thé çöürsé ÌD ïs ïnvälïd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
#: ecommerce/static/js/utils/validation_patterns.js
msgid "The product name cannot contain HTML."
msgstr ""
"Thé prödüçt nämé çännöt çöntäïn HTML. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, "
"¢σηѕє¢тєтυ#"
#: ecommerce/static/js/views/coupon_detail_view.js #: ecommerce/static/js/views/coupon_detail_view.js
#: ecommerce/static/js/views/coupon_form_view.js #: ecommerce/static/js/views/coupon_form_view.js
msgid "Can be used once by one customer" msgid "Can be used once by one customer"
...@@ -428,17 +425,20 @@ msgid "Professional Education" ...@@ -428,17 +425,20 @@ msgid "Professional Education"
msgstr "Pröféssïönäl Édüçätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#" msgstr "Pröféssïönäl Édüçätïön Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢#"
#: ecommerce/static/js/views/course_form_view.js #: ecommerce/static/js/views/course_form_view.js
msgid "Paid certificate track with initial verification and Professional " msgid ""
"Paid certificate track with initial verification and Professional Education "
"Certificate"
msgstr "" msgstr ""
"Päïd çértïfïçäté träçk wïth ïnïtïäl vérïfïçätïön änd Pröféssïönäl Ⱡ'σяєм " "Päïd çértïfïçäté träçk wïth ïnïtïäl vérïfïçätïön änd Pröféssïönäl Édüçätïön "
"ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #" "Çértïfïçäté Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє#"
#: ecommerce/static/js/views/course_form_view.js #: ecommerce/static/js/views/course_form_view.js
msgid "" msgid ""
"Paid certificate track with initial verification and Verified Certificate, " "Paid certificate track with initial verification and Verified Certificate, "
"and option to purchase credit"
msgstr "" msgstr ""
"Päïd çértïfïçäté träçk wïth ïnïtïäl vérïfïçätïön änd Vérïfïéd Çértïfïçäté, " "Päïd çértïfïçäté träçk wïth ïnïtïäl vérïfïçätïön änd Vérïfïéd Çértïfïçäté, "
"Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυ#" "änd öptïön tö pürçhäsé çrédït Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α#"
#. Translators: _START_, _END_, and _TOTAL_ are placeholders. Do NOT translate #. Translators: _START_, _END_, and _TOTAL_ are placeholders. Do NOT translate
#. them. #. them.
......
...@@ -47,6 +47,23 @@ def get_lms_courseware_url(course_run_id): ...@@ -47,6 +47,23 @@ def get_lms_courseware_url(course_run_id):
return get_lms_url('courses/{}/info'.format(course_run_id)) return get_lms_url('courses/{}/info'.format(course_run_id))
def get_lms_course_about_url(course_key):
"""
Return the courseware about URL for the given course key.
Returns:
string: The course about page URL.
"""
return get_lms_url('courses/{}/about'.format(course_key))
def get_lms_explore_courses_url():
"""
Return the explore courses url.
"""
return get_lms_url('courses')
def get_lms_dashboard_url(): def get_lms_dashboard_url():
site_configuration = _get_site_configuration() site_configuration = _get_site_configuration()
return site_configuration.student_dashboard_url return site_configuration.student_dashboard_url
......
...@@ -19,7 +19,7 @@ from requests.exceptions import ConnectionError, Timeout ...@@ -19,7 +19,7 @@ from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import SlumberBaseException from slumber.exceptions import SlumberBaseException
from ecommerce.core.exceptions import SiteConfigurationError from ecommerce.core.exceptions import SiteConfigurationError
from ecommerce.core.url_utils import get_lms_url from ecommerce.core.url_utils import get_lms_course_about_url, get_lms_url
from ecommerce.courses.utils import get_certificate_type_display_value, get_course_info_from_catalog from ecommerce.courses.utils import get_certificate_type_display_value, get_course_info_from_catalog
from ecommerce.enterprise.entitlements import get_enterprise_code_redemption_redirect from ecommerce.enterprise.entitlements import get_enterprise_code_redemption_redirect
from ecommerce.enterprise.utils import CONSENT_FAILED_PARAM, get_enterprise_customer_from_voucher, has_enterprise_offer from ecommerce.enterprise.utils import CONSENT_FAILED_PARAM, get_enterprise_customer_from_voucher, has_enterprise_offer
...@@ -202,6 +202,26 @@ class BasketSummaryView(BasketView): ...@@ -202,6 +202,26 @@ class BasketSummaryView(BasketView):
except (ConnectionError, SlumberBaseException, Timeout): except (ConnectionError, SlumberBaseException, Timeout):
logger.exception('Failed to retrieve data from Discovery Service for course [%s].', course_key) logger.exception('Failed to retrieve data from Discovery Service for course [%s].', course_key)
if self.request.basket.num_items == 1 and product.is_enrollment_code_product:
course_key = CourseKey.from_string(product.attr.course_key)
course_about = get_lms_course_about_url(course_key=course_key)
messages.info(
self.request,
_(
'{strong_start}Purchasing access just for yourself?{strong_end}{paragraph_start}If you are '
'purchasing a single code for someone else, please continue with checkout. However, if you are the '
'learner {link_start}go back{link_end} to enroll directly.{paragraph_end}'
).format(
strong_start='<strong>',
strong_end='</strong>',
paragraph_start='<p>',
paragraph_end='</p>',
link_start='<a href="{course_about}">'.format(course_about=course_about),
link_end='</a>'
),
extra_tags='safe'
)
return { return {
'product_title': course_name, 'product_title': course_name,
'course_key': course_key, 'course_key': course_key,
...@@ -259,8 +279,26 @@ class BasketSummaryView(BasketView): ...@@ -259,8 +279,26 @@ class BasketSummaryView(BasketView):
line_data = self._get_course_data(line.product) line_data = self._get_course_data(line.product)
show_voucher_form = False show_voucher_form = False
order_details_msg = _( order_details_msg = _(
'You will receive an email at {user_email} with your enrollment code(s).' '{paragraph_start}By purchasing, you and your organization agree to the following terms:'
).format(user_email=self.request.user.email) '{paragraph_end} {ul_start} {li_start}Each code is valid for the one course covered and can be '
'used only one time.{li_end} {li_start}You are responsible for distributing codes to your learners.'
'{li_end} {li_start}Each code will expire in one year from date of purchase or, if earlier, once '
'the course is closed.{li_end} {li_start}If a course is not designated as self-paced, you should '
'confirm that a course run is available before expiration. {li_end} {li_start}You may not resell '
'codes to third parties.{li_end} {ul_end} {strong_start}All sales final. No refunds.{strong_end} '
'{paragraph_start}You will receive an email at {user_email} with your enrollment code(s). '
'{paragraph_end}'
).format(
strong_start='<strong>',
strong_end='</strong>',
paragraph_start='<p>',
paragraph_end='</p>',
ul_start='<ul>',
li_start='<li>',
li_end='</li>',
ul_end='</ul>',
user_email=self.request.user.email
)
else: else:
line_data = { line_data = {
'product_title': line.product.title, 'product_title': line.product.title,
...@@ -401,14 +439,16 @@ class BasketSummaryView(BasketView): ...@@ -401,14 +439,16 @@ class BasketSummaryView(BasketView):
) )
except ValueError: except ValueError:
total_benefit = None total_benefit = None
num_of_items = self.request.basket.num_items
context.update({ context.update({
'formset_lines_data': zip(formset, lines_data), 'formset_lines_data': zip(formset, lines_data),
'free_basket': context['order_total'].incl_tax == 0, 'free_basket': context['order_total'].incl_tax == 0,
'homepage_url': get_lms_url(''), 'homepage_url': get_lms_url(''),
'min_seat_quantity': 1, 'min_seat_quantity': 1,
'max_seat_quantity': 100,
'payment_processors': payment_processors, 'payment_processors': payment_processors,
'total_benefit': total_benefit 'total_benefit': total_benefit,
'line_price': (self.request.basket.total_incl_tax_excl_discounts / num_of_items) if num_of_items > 0 else 0
}) })
return context return context
......
...@@ -12,7 +12,12 @@ from django.views.generic import RedirectView, TemplateView ...@@ -12,7 +12,12 @@ from django.views.generic import RedirectView, TemplateView
from oscar.apps.checkout.views import * # pylint: disable=wildcard-import, unused-wildcard-import from oscar.apps.checkout.views import * # pylint: disable=wildcard-import, unused-wildcard-import
from oscar.core.loading import get_class, get_model from oscar.core.loading import get_class, get_model
from ecommerce.core.url_utils import get_lms_courseware_url, get_lms_dashboard_url, get_lms_program_dashboard_url from ecommerce.core.url_utils import (
get_lms_courseware_url,
get_lms_dashboard_url,
get_lms_explore_courses_url,
get_lms_program_dashboard_url
)
from ecommerce.enterprise.utils import has_enterprise_offer from ecommerce.enterprise.utils import has_enterprise_offer
from ecommerce.extensions.checkout.exceptions import BasketNotFreeError from ecommerce.extensions.checkout.exceptions import BasketNotFreeError
from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin
...@@ -183,6 +188,12 @@ class ReceiptResponseView(ThankYouView): ...@@ -183,6 +188,12 @@ class ReceiptResponseView(ThankYouView):
}) })
context.update(self.get_order_dashboard_context(order)) context.update(self.get_order_dashboard_context(order))
context.update(self.get_order_verification_context(order)) context.update(self.get_order_verification_context(order))
context.update({
'explore_courses_url': get_lms_explore_courses_url(),
'has_enrollment_code_product': any(
line.product.is_enrollment_code_product for line in order.basket.all_lines()
)
})
return context return context
def get_object(self): def get_object(self):
......
...@@ -44,6 +44,9 @@ class PaymentForm(forms.Form): ...@@ -44,6 +44,9 @@ class PaymentForm(forms.Form):
def __init__(self, user, request, *args, **kwargs): def __init__(self, user, request, *args, **kwargs):
super(PaymentForm, self).__init__(*args, **kwargs) super(PaymentForm, self).__init__(*args, **kwargs)
self.request = request self.request = request
self.basket_has_enrollment_code_product = any(
line.product.is_enrollment_code_product for line in self.request.basket.all_lines()
)
update_basket_queryset_filter(self, user) update_basket_queryset_filter(self, user)
self.helper = FormHelper(self) self.helper = FormHelper(self)
...@@ -100,6 +103,20 @@ class PaymentForm(forms.Form): ...@@ -100,6 +103,20 @@ class PaymentForm(forms.Form):
self.fields[bound_field.name].label = _('{label} (required)').format(label=bound_field.label) self.fields[bound_field.name].label = _('{label} (required)').format(label=bound_field.label)
bound_field.field.widget.attrs['required'] = 'required' bound_field.field.widget.attrs['required'] = 'required'
if self.basket_has_enrollment_code_product and 'organization' not in self.fields:
# If basket has any enrollment code items then we will add an organization
# field next to "last_name."
self.fields['organization'] = forms.CharField(max_length=60, label=_('Organization (required)'))
organization_div = Div(
Div(
Div('organization'),
HTML('<p class="help-block"></p>'),
css_class='form-item col-md-6'
),
css_class='row'
)
self.helper.layout.fields.insert(self.fields.keys().index('last_name') + 1, organization_div)
basket = forms.ModelChoiceField( basket = forms.ModelChoiceField(
queryset=Basket.objects.all(), queryset=Basket.objects.all(),
widget=forms.HiddenInput(), widget=forms.HiddenInput(),
...@@ -108,16 +125,16 @@ class PaymentForm(forms.Form): ...@@ -108,16 +125,16 @@ class PaymentForm(forms.Form):
'invalid_choice': _('There was a problem retrieving your basket. Refresh the page to try again.'), 'invalid_choice': _('There was a problem retrieving your basket. Refresh the page to try again.'),
} }
) )
first_name = forms.CharField(max_length=60, label=_('First Name')) first_name = forms.CharField(max_length=60, label=_('First Name (required)'))
last_name = forms.CharField(max_length=60, label=_('Last Name')) last_name = forms.CharField(max_length=60, label=_('Last Name (required)'))
address_line1 = forms.CharField(max_length=60, label=_('Address'), required=False) address_line1 = forms.CharField(max_length=60, label=_('Address (required)'), required=False)
address_line2 = forms.CharField(max_length=29, required=False, label=_('Suite/Apartment Number')) address_line2 = forms.CharField(max_length=29, required=False, label=_('Suite/Apartment Number'))
city = forms.CharField(max_length=32, label=_('City')) city = forms.CharField(max_length=32, label=_('City (required)'))
# max_length for state field is set to default 60, if it needs to be changed, # max_length for state field is set to default 60, if it needs to be changed,
# the equivalent (maxlength) attribute in the basket page JS code needs to be changed too. # the equivalent (maxlength) attribute in the basket page JS code needs to be changed too.
state = forms.CharField(max_length=60, required=False, label=_('State/Province')) state = forms.CharField(max_length=60, required=False, label=_('State/Province'))
postal_code = forms.CharField(max_length=10, required=False, label=_('Zip/Postal Code')) postal_code = forms.CharField(max_length=10, required=False, label=_('Zip/Postal Code'))
country = forms.ChoiceField(choices=country_choices, label=_('Country')) country = forms.ChoiceField(choices=country_choices, label=_('Country (required)'))
def clean_basket(self): def clean_basket(self):
basket = self.cleaned_data['basket'] basket = self.cleaned_data['basket']
......
...@@ -2,12 +2,19 @@ from __future__ import unicode_literals ...@@ -2,12 +2,19 @@ from __future__ import unicode_literals
import ddt import ddt
import pycountry import pycountry
from oscar.core.loading import get_model
from oscar.test import factories
from waffle.models import Switch from waffle.models import Switch
from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME, ENROLLMENT_CODE_SWITCH
from ecommerce.core.tests import toggle_switch
from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.extensions.payment.forms import PaymentForm from ecommerce.extensions.payment.forms import PaymentForm
from ecommerce.extensions.test.factories import create_basket from ecommerce.extensions.test.factories import create_basket
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
Product = get_model('catalogue', 'Product')
@ddt.ddt @ddt.ddt
class PaymentFormTests(TestCase): class PaymentFormTests(TestCase):
...@@ -16,6 +23,29 @@ class PaymentFormTests(TestCase): ...@@ -16,6 +23,29 @@ class PaymentFormTests(TestCase):
self.user = self.create_user() self.user = self.create_user()
self.basket = create_basket(owner=self.user) self.basket = create_basket(owner=self.user)
def create_basket_and_add_product(self, product):
basket = factories.BasketFactory(owner=self.user, site=self.site)
basket.add_product(product, 1)
return basket
def prepare_course_seat_and_enrollment_code(self, seat_type='verified', id_verification=False):
"""Helper function that creates a new course, enables enrollment codes and creates a new
seat and enrollment code for it.
Args:
seat_type (str): Seat/certification type.
is_verification (bool): Whether or not id verification is required for the seat.
Returns:
The newly created course, seat and enrollment code.
"""
course = CourseFactory()
toggle_switch(ENROLLMENT_CODE_SWITCH, True)
self.site.siteconfiguration.enable_enrollment_codes = True
self.site.siteconfiguration.save()
seat = course.create_or_update_seat(seat_type, id_verification, 10, self.partner, create_enrollment_code=True)
enrollment_code = Product.objects.get(product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME)
return course, seat, enrollment_code
def _generate_data(self, **kwargs): def _generate_data(self, **kwargs):
data = { data = {
'basket': self.basket.id, 'basket': self.basket.id,
...@@ -132,3 +162,45 @@ class PaymentFormTests(TestCase): ...@@ -132,3 +162,45 @@ class PaymentFormTests(TestCase):
actual = list(form.fields['country'].choices) actual = list(form.fields['country'].choices)
actual.pop(0) # Remove the "Choose country" placeholder actual.pop(0) # Remove the "Choose country" placeholder
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
def test_organization_field_in_form(self):
""" Verify the field 'organization' is present in the form when the basket has an enrollment code product. """
__, __, enrollment_code = self.prepare_course_seat_and_enrollment_code()
basket = self.create_basket_and_add_product(enrollment_code)
self.request.basket = basket
data = {
'basket': basket.id,
'first_name': 'Test',
'last_name': 'User',
'address_line1': '141 Portland Ave.',
'address_line2': 'Floor 9',
'city': 'Cambridge',
'state': 'MA',
'postal_code': '02139',
'country': 'US',
}
form = PaymentForm(user=self.user, data=data, request=self.request)
self.assertTrue('organization' in form.fields)
def test_organization_field_not_in_form(self):
"""
Verify the field 'organization' is not present in the form when the basket does not have an enrollment
code product.
"""
course = CourseFactory()
product1 = course.create_or_update_seat("Verified", True, 0, self.partner)
basket = self.create_basket_and_add_product(product1)
self.request.basket = basket
data = {
'basket': basket.id,
'first_name': 'Test',
'last_name': 'User',
'address_line1': '141 Portland Ave.',
'address_line2': 'Floor 9',
'city': 'Cambridge',
'state': 'MA',
'postal_code': '02139',
'country': 'US',
}
form = PaymentForm(user=self.user, data=data, request=self.request)
self.assertFalse('organization' in form.fields)
/* jshint -W065 */
define([ define([
'jquery', 'jquery',
'underscore', 'underscore',
...@@ -79,6 +80,7 @@ define([ ...@@ -79,6 +80,7 @@ define([
'input[name=first_name]', 'input[name=first_name]',
'input[name=last_name]', 'input[name=last_name]',
'input[name=city]', 'input[name=city]',
'input[name=organization]',
'select[name=country]' 'select[name=country]'
], ],
countriesWithRequiredStateAndPostalCodeValues = ['US', 'CA'], countriesWithRequiredStateAndPostalCodeValues = ['US', 'CA'],
...@@ -148,6 +150,24 @@ define([ ...@@ -148,6 +150,24 @@ define([
} }
}, },
showErrorState: function(e, msg) {
e.preventDefault();
$('#input-quantity-field').addClass('error-state');
$('div#error-msg').text(gettext(msg));
},
validateQuantity: function(e) {
var inputQuantity = $('#input-quantity-field').val();
var quantity = isNaN(parseInt(inputQuantity, 10)) ? '' : parseInt(inputQuantity, 10);
if (quantity === '') {
this.showErrorState(e, 'Please enter a quantity from 1 to 100.');
} else if (quantity > 100) {
this.showErrorState(e, 'Quantity must be less than or equal to 100.');
} else if (quantity < 1) {
this.showErrorState(e, 'Quantity must be greater than or equal to 1.');
}
},
detectCreditCard: function() { detectCreditCard: function() {
var card, var card,
$input = $('#card-number'), $input = $('#card-number'),
...@@ -483,6 +503,10 @@ define([ ...@@ -483,6 +503,10 @@ define([
BasketPage.detectCreditCard(); BasketPage.detectCreditCard();
}); });
$('#quantity-update').on('click', function(e) {
BasketPage.validateQuantity(e);
});
$('#payment-button').click(function(e) { $('#payment-button').click(function(e) {
_.each($('.help-block'), function(errorMsg) { _.each($('.help-block'), function(errorMsg) {
$(errorMsg).empty(); // Clear existing validation error messages. $(errorMsg).empty(); // Clear existing validation error messages.
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
<div aria-labelledby="order-details-region"></div> <div aria-labelledby="order-details-region"></div>
<div class="spinner"> <div class="spinner">
<input class="quantity" id="id_form-0-quantity" min="1" max="10" name="form-0-quantity" type="number" value="1"> <input class="quantity" id="input-quantity-field" min="1" max="10" name="form-0-quantity" type="number" value="1">
<div class="input-group-btn-vertical"> <div class="input-group-btn-vertical">
<button class="btn btn-primary" type="button"> <button class="btn btn-primary" type="button">
...@@ -36,6 +36,8 @@ ...@@ -36,6 +36,8 @@
</button> </button>
</div> </div>
</div> </div>
<button id="quantity-update" class="update-button quantity-update" type="submit" data-loading-text="Updating...">Update</button>
<div id="error-msg"></div>
<div id="voucher_form_container"> <div id="voucher_form_container">
<input id="id_code"> <input id="id_code">
......
...@@ -153,6 +153,30 @@ define([ ...@@ -153,6 +153,30 @@ define([
expect($quantity.first().val()).toEqual('1'); expect($quantity.first().val()).toEqual('1');
}); });
it('should show error message when quantity is greater than 100', function() {
BasketPage.onReady();
$('input.quantity').first().val(101);
$('#quantity-update').trigger('click');
expect($('div#error-msg').text()).toEqual('Quantity must be less than or equal to 100.');
});
it('should show error message when quantity is less than 1', function() {
BasketPage.onReady();
$('input.quantity').first().val(0);
$('#quantity-update').trigger('click');
expect($('div#error-msg').text()).toEqual('Quantity must be greater than or equal to 1.');
});
it('should show error message when quantity is not given', function() {
BasketPage.onReady();
$('input.quantity').first().val('');
$('#quantity-update').trigger('click');
expect($('div#error-msg').text()).toEqual('Please enter a quantity from 1 to 100.');
});
it('should recognize the credit card', function() { it('should recognize the credit card', function() {
var validCardList = [ var validCardList = [
{number: '378282246310005', name: 'amex'}, {number: '378282246310005', name: 'amex'},
......
...@@ -480,11 +480,31 @@ ...@@ -480,11 +480,31 @@
.quantity-group { .quantity-group {
padding-left: 0; padding-left: 0;
.max-quantity {
font-size: 10px;
color: #4a4a4a;
font-weight: 600;
margin: 0 0 0 85px;
display: block;
}
#error-msg {
font-size: 12px;
color: red;
margin: 5px 0 5px 85px;
}
.error-state {
border-color: red;
}
} }
.quantity-label { .quantity-label {
display: inline-block; display: inline-block;
padding-right: 10px; padding-right: 10px;
vertical-align: top;
margin: 5px 0 0;
} }
.checkout-quantity { .checkout-quantity {
...@@ -492,7 +512,7 @@ ...@@ -492,7 +512,7 @@
} }
.spinner { .spinner {
width: 70px; width: 80px;
float: left; float: left;
padding-right: 10px; padding-right: 10px;
} }
...@@ -519,18 +539,31 @@ ...@@ -519,18 +539,31 @@
} }
.amount { .amount {
padding-bottom: 20px; padding: 0;
color: $gray-color; color: $gray-color;
font-size: 18px; font-size: 18px;
span {
float: none !important;
display: block;
text-align: unset;
.price {
font-weight: bold; &:first-child {
margin: 0 0 20px;
}
} }
} }
#basket-total { #basket-total {
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
.price {
font-weight: bold;
text-align: unset;
padding: 0;
}
} }
#voucher-information { #voucher-information {
......
...@@ -152,6 +152,10 @@ ...@@ -152,6 +152,10 @@
padding: 10px 15px; padding: 10px 15px;
} }
} }
#find-more-courses-link {
margin: 20px;
float: right;
}
.dashboard-link { .dashboard-link {
line-height: 20px; line-height: 20px;
......
...@@ -36,11 +36,18 @@ ...@@ -36,11 +36,18 @@
{% captureas link_start %} {% captureas link_start %}
<a href="mailto:{{ order.user.email }}"> <a href="mailto:{{ order.user.email }}">
{% endcaptureas %} {% endcaptureas %}
{% blocktrans trimmed with email=order.user.email link_end="</a>" %} {% if has_enrollment_code_product %}
Your order is complete. If you need a receipt, you can print this page. {% blocktrans trimmed with email=order.user.email link_end="</a>" %}
You will also receive a confirmation message with this information at Your order is complete. You will receive a confirmation message and your enrollment codes(s) at
{{ link_start }}{{ email }}{{ link_end }}. {{ link_start }}{{ email }}{{ link_end }}. If you need a receipt, you can print this page.
{% endblocktrans %} {% endblocktrans %}
{% else %}
{% blocktrans trimmed with email=order.user.email link_end="</a>" %}
Your order is complete. If you need a receipt, you can print this page.
You will also receive a confirmation message with this information at
{{ link_start }}{{ email }}{{ link_end }}.
{% endblocktrans %}
{% endif %}
</div> </div>
{% if order.billing_address %} {% if order.billing_address %}
...@@ -86,7 +93,7 @@ ...@@ -86,7 +93,7 @@
{% endif %} {% endif %}
</dd> </dd>
<dt class="line-price sr">{% trans "Item Price:" %}</dt> <dt class="line-price sr">{% trans "Item Price:" %}</dt>
<dd class="line-price price">{{ line.line_price_before_discounts_incl_tax|currency:order.currency }}</dd> <dd class="line-price price">{{ line.unit_price_incl_tax|currency:order.currency }}</dd>
</div> </div>
{% endfor %} {% endfor %}
</dl> </dl>
...@@ -165,6 +172,11 @@ ...@@ -165,6 +172,11 @@
</a> </a>
</div> </div>
{% endif %} {% endif %}
<div id="find-more-courses-link">
<a class="find-more-courses-link nav-link" href="{{ explore_courses_url }}" target="_blank">
{% trans "Find more courses" %}
</a>
</div>
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}
{% load i18n %}
{% load currency_filters %}
<p>
{% if basket.is_empty %}
{% trans "Your basket is now empty" %}
{% else %}
{% if basket.is_tax_known %}
{% blocktrans with num_items=basket.num_items total=basket.total_incl_tax|currency:basket.currency strong_start='<b>' strong_end='</b>' paragraph_start='<p>' paragraph_end='</p>' %}
{{ strong_start }}We’ve updated your quantity.{{ strong_end }}
{{ paragraph_start }}Your cart includes {{ num_items }} enrollment codes at a total cost of {{ total }}, that you will receive via email.{{ paragraph_end }}
{% endblocktrans %}
{% else %}
{% blocktrans with num_items=basket.num_items total=basket.total_excl_tax|currency:basket.currency strong_start='<b>' strong_end='</b>' paragraph_start='<p>' paragraph_end='</p>' %}
{{ strong_start }}We’ve updated your quantity.{{ strong_end }}
{{ paragraph_start }}Your cart includes {{ num_items }} enrollment codes at a total cost of {{ total }}, that you will receive via email.{{ paragraph_end }}
{% endblocktrans %}
{% endif %}
{% endif %}
</p>
{% if include_buttons %}
<p>
<a href="{% url 'basket:summary' %}" class="btn btn-info">{% trans "View basket" %}</a>
<a href="{% url 'checkout:index' %}" class="btn btn-info">{% trans "Checkout now" %}</a>
</p>
{% endif %}
...@@ -27,14 +27,16 @@ ...@@ -27,14 +27,16 @@
</div> </div>
{% if line_data.enrollment_code %} {% if line_data.enrollment_code %}
<div class="col-sm-12 col-xs-12 form-inline quantity-group"> <div class="col-sm-12 col-xs-12 form-inline quantity-group">
<label class="product-price-label text-muted quantity-label">{% trans 'Quantity' %}</label> <label for="input-quantity-field " class="product-price-label text-muted quantity-label">{% trans 'Quantity' %}</label>
<div class="checkout-quantity form-group"> <div class="checkout-quantity form-group">
<div class="input-group spinner {% if form.errors %}error{% endif %}"> <div class="input-group spinner {% if form.errors %}error{% endif %}">
{% render_field form.quantity class+="quantity form-control" min=min_seat_quantity %} {% render_field form.quantity class+="quantity form-control" min=min_seat_quantity max=max_seat_quantity title="Quantity" id="input-quantity-field" %}
</div> </div>
<button class="btn btn-primary update-button quantity-update" type="submit" <button id="quantity-update" class="btn btn-primary update-button quantity-update" type="submit"
data-loading-text="{% trans 'Updating...' %}">{% trans "Update" %}</button> data-loading-text="{% trans 'Updating...' %}">{% trans "Update" %}</button>
</div> </div>
<div id="error-msg" role="alert"></div>
<span class="max-quantity">Max: 100</span>
</div> </div>
{% endif %} {% endif %}
</div> </div>
...@@ -43,9 +45,19 @@ ...@@ -43,9 +45,19 @@
</form> </form>
<div role="region" aria-labelledby="summary-region" aria-live="polite"> <div role="region" aria-labelledby="summary-region" aria-live="polite">
<h2 id="summary-region" class="title">{% trans "summary" %}</h2> <h2 id="summary-region" class="title">{% trans "summary" %}</h2>
<div id="line-price" class="amount row"> <div class="row">
<span class="description">{% trans "Price" %}</span> <div id="line-price" class="amount col-xs-4 col-sm-4">
<span class="price">{{basket.total_incl_tax_excl_discounts|currency:basket.currency}}</span> <span class="description">{% trans "Price" %}</span>
<span class="price">{{line_price|currency:basket.currency}}</span>
</div>
<div id="line-price" class="amount col-xs-4 col-sm-4">
<span class="description float-none">{% trans "Quanity" %}</span>
<span>{{basket.num_items}}</span>
</div>
<div id="line-price" class="amount col-xs-4 col-sm-4">
<span class="description">{% trans "Subtotal" %}</span>
<span class="price">{{order_total.incl_tax|currency:basket.currency}}</span>
</div>
</div> </div>
{% if basket.total_discount %} {% if basket.total_discount %}
<div id="line-discount" class="amount row"> <div id="line-discount" class="amount row">
...@@ -98,14 +110,15 @@ ...@@ -98,14 +110,15 @@
{% endif %} {% endif %}
</div> </div>
<div id="basket-total" class="row" aria-describedby="total-price-desc"> <div id="basket-total" class="row" aria-describedby="total-price-desc">
<span id="total-price-desc" class="description">{% trans "TOTAL" %}</span> <span id="total-price-desc col-xs-4 col-sm-4" class="description">{% trans "TOTAL" %}</span>
<span class="price">{{ order_total.incl_tax|currency:basket.currency }}</span> <span class="col-xs-4 col-sm-4"></span>
<span class="price col-xs-4 col-sm-4">{{ order_total.incl_tax|currency:basket.currency }}</span>
</div> </div>
</div> </div>
{% if order_details_msg %} {% if order_details_msg %}
<div role="region" aria-labelledby="order-details-region"> <div role="region" aria-labelledby="order-details-region">
<h2 id="order-details-region" class="title">{% trans "order details" %}</h2> <h2 id="order-details-region" class="title">{% trans "order details" %}</h2>
<p>{{ order_details_msg }}</p> <p>{{ order_details_msg | safe}}</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment