Repository: django-oscar/django-oscar

Issue:
Issues/808/shipping method view
Fixes #1781 



Discussion:

Resolving patch:
diff --git a/src/oscar/apps/checkout/forms.py b/src/oscar/apps/checkout/forms.py
--- a/src/oscar/apps/checkout/forms.py
+++ b/src/oscar/apps/checkout/forms.py
@@ -40,6 +40,14 @@ class Meta:
             'phone_number', 'notes',
         ]
 
+class ShippingMethodForm(forms.Form):
+    method_code = forms.ChoiceField(widget=forms.HiddenInput)
+
+    def __init__(self, *args, **kwargs):
+        methods = kwargs.pop('methods', [])
+        super(ShippingMethodForm, self).__init__(*args, **kwargs)
+        self.fields['method_code'].choices = ((m.code, m.name) for m in methods)
+
 
 class GatewayForm(AuthenticationForm):
     username = forms.EmailField(label=_("My email address is"))
diff --git a/src/oscar/apps/checkout/views.py b/src/oscar/apps/checkout/views.py
--- a/src/oscar/apps/checkout/views.py
+++ b/src/oscar/apps/checkout/views.py
@@ -14,8 +14,8 @@
 from oscar.core.loading import get_class, get_classes, get_model
 from . import signals
 
-ShippingAddressForm, GatewayForm \
-    = get_classes('checkout.forms', ['ShippingAddressForm', 'GatewayForm'])
+ShippingAddressForm, ShippingMethodForm, GatewayForm \
+    = get_classes('checkout.forms', ['ShippingAddressForm', 'ShippingMethodForm', 'GatewayForm'])
 OrderCreator = get_class('order.utils', 'OrderCreator')
 UserAddressForm = get_class('address.forms', 'UserAddressForm')
 Repository = get_class('shipping.repository', 'Repository')
@@ -233,7 +233,7 @@ def get_success_url(self):
 # ===============
 
 
-class ShippingMethodView(CheckoutSessionMixin, generic.TemplateView):
+class ShippingMethodView(CheckoutSessionMixin, generic.FormView):
     """
     View for allowing a user to choose a shipping method.
 
@@ -246,10 +246,15 @@ class ShippingMethodView(CheckoutSessionMixin, generic.TemplateView):
     the user can choose the appropriate one.
     """
     template_name = 'checkout/shipping_methods.html'
+    form_class = ShippingMethodForm
     pre_conditions = ['check_basket_is_not_empty',
                       'check_basket_is_valid',
                       'check_user_email_is_captured']
 
+    def post(self, request, *args, **kwargs):
+        self._methods = self.get_available_shipping_methods()
+        return super(ShippingMethodView, self).post(request, *args, **kwargs)
+
     def get(self, request, *args, **kwargs):
         # These pre-conditions can't easily be factored out into the normal
         # pre-conditions as they do more than run a test and then raise an
@@ -291,6 +296,11 @@ def get_context_data(self, **kwargs):
         kwargs['methods'] = self._methods
         return kwargs
 
+    def get_form_kwargs(self):
+        kwargs = super(ShippingMethodView, self).get_form_kwargs()
+        kwargs['methods'] = self._methods
+        return kwargs
+
     def get_available_shipping_methods(self):
         """
         Returns all applicable shipping method objects for a given basket.
@@ -304,26 +314,17 @@ def get_available_shipping_methods(self):
             shipping_addr=self.get_shipping_address(self.request.basket),
             request=self.request)
 
-    def is_valid_shipping_method(self, method_code):
-        for method in self.get_available_shipping_methods():
-            if method.code == method_code:
-                return True
-        return False
-
-    def post(self, request, *args, **kwargs):
-        # Need to check that this code is valid for this user
-        method_code = request.POST.get('method_code', None)
-        if not self.is_valid_shipping_method(method_code):
-            messages.error(request, _("Your submitted shipping method is not"
-                                      " permitted"))
-            return redirect('checkout:shipping-method')
-
+    def form_valid(self, form):
         # Save the code for the chosen shipping method in the session
         # and continue to the next step.
-        self.checkout_session.use_shipping_method(method_code)
-
+        self.checkout_session.use_shipping_method(form.cleaned_data['method_code'])
         return self.get_success_response()
 
+    def form_invalid(self, form):
+        messages.error(self.request, _("Your submitted shipping method is not"
+                                       " permitted"))
+        return super(ShippingMethodView, self).form_invalid(form)
+
     def get_success_response(self):
         return redirect('checkout:payment-method')
 


Test patch:
diff --git a/src/oscar/test/testcases.py b/src/oscar/test/testcases.py
--- a/src/oscar/test/testcases.py
+++ b/src/oscar/test/testcases.py
@@ -73,6 +73,12 @@ def assertIsRedirect(self, response, expected_url=None):
             location = URL.from_string(response['Location'])
             self.assertEqual(expected_url, location.path())
 
+    def assertIsNotRedirect(self, response):
+        self.assertIsOk(response)
+        self.assertTrue(response.status_code not in (
+            http_client.FOUND, http_client.MOVED_PERMANENTLY
+        ))
+
     def assertRedirectsTo(self, response, url_name, kwargs=None):
         """
         Asserts that a response is a redirect to a given URL name.
diff --git a/tests/functional/checkout/guest_checkout_tests.py b/tests/functional/checkout/guest_checkout_tests.py
--- a/tests/functional/checkout/guest_checkout_tests.py
+++ b/tests/functional/checkout/guest_checkout_tests.py
@@ -254,7 +254,8 @@ def test_check_user_can_submit_only_valid_shipping_method(self, mock_repo):
         # a malicious attempt?
         form_page.forms[0]['method_code'].value = 'super-free-shipping'
         response = form_page.forms[0].submit()
-        self.assertRedirectsTo(response, 'checkout:shipping-method')
+        self.assertIsNotRedirect(response)
+        response.mustcontain('Your submitted shipping method is not permitted')
 
 
 @override_settings(OSCAR_ALLOW_ANON_CHECKOUT=True)