def test_bytes_conversion(self): settings.clear_cache() register_setting(name="BYTES_TEST_SETTING", editable=True, default=b"") Setting.objects.create(name="BYTES_TEST_SETTING", value="A unicode value") self.assertEqual(settings.BYTES_TEST_SETTING, b"A unicode value")
def send_order_email(request, order): """ Send order receipt email on successful order. """ settings.clear_cache() order_context = { "order": order, "request": request, "order_items": order.items.all() } order_context.update(order.details_as_dict()) try: get_template("shop/email/order_receipt.html") except TemplateDoesNotExist: receipt_template = "email/order_receipt" else: receipt_template = "shop/email/order_receipt" from warnings import warn warn("Shop email receipt templates have moved from " "templates/shop/email/ to templates/email/") send_mail_template(settings.SHOP_ORDER_EMAIL_SUBJECT, receipt_template, settings.SHOP_ORDER_FROM_EMAIL, order.billing_detail_email, context=order_context, addr_bcc=settings.SHOP_ORDER_EMAIL_BCC or None)
def test_settings(self): """ Test that an editable setting can be overridden with a DB value and that the data type is preserved when the value is returned back out of the DB. Also checks to ensure no unsupported types are defined for editable settings. """ settings.clear_cache() # Find an editable setting for each supported type. names_by_type = {} for setting in registry.values(): if setting["editable"] and setting["type"] not in names_by_type: names_by_type[setting["type"]] = setting["name"] # Create a modified value for each setting and save it. values_by_name = {} for (setting_type, setting_name) in names_by_type.items(): setting_value = registry[setting_name]["default"] if setting_type in (int, float): setting_value += 1 elif setting_type is bool: setting_value = not setting_value elif setting_type is str: setting_value += u"test" elif setting_type is bytes: setting_value += b"test" else: setting = "%s: %s" % (setting_name, setting_type) self.fail("Unsupported setting type for %s" % setting) values_by_name[setting_name] = setting_value Setting.objects.create(name=setting_name, value=setting_value) # Load the settings and make sure the DB values have persisted. for (name, value) in values_by_name.items(): self.assertEqual(getattr(settings, name), value)
def test_default_handler_exists(self): """ Ensure that the handler specified in default settings exists as well as the default setting itself. """ settings.clear_cache() handler = lambda s: import_dotted_path(s) if s else lambda *args: None self.assertTrue(handler(settings.SHOP_HANDLER_TAX) is not None)
def cart( request, template="shop/cart.html", cart_formset_class=CartItemFormSet, discount_form_class=DiscountForm, extra_context=None, ): """ Display cart and handle removing items from the cart. """ cart_formset = cart_formset_class(instance=request.cart) discount_form = discount_form_class(request, request.POST or None) if request.method == "POST": valid = True if request.POST.get("update_cart"): valid = request.cart.has_items() if not valid: # Session timed out. info(request, _("Your cart has expired")) else: cart_formset = cart_formset_class(request.POST, instance=request.cart) valid = cart_formset.is_valid() if valid: cart_formset.save() recalculate_cart(request) info(request, _("Cart updated")) else: # Reset the cart formset so that the cart # always indicates the correct quantities. # The user is shown their invalid quantity # via the error message, which we need to # copy over to the new formset here. errors = cart_formset._errors cart_formset = cart_formset_class(instance=request.cart) cart_formset._errors = errors else: valid = discount_form.is_valid() if valid: discount_form.set_discount() # Potentially need to set shipping if a discount code # was previously entered with free shipping, and then # another was entered (replacing the old) without # free shipping, *and* the user has already progressed # to the final checkout step, which they'd go straight # to when returning to checkout, bypassing billing and # shipping details step where shipping is normally set. recalculate_cart(request) if valid: return redirect("shop_cart") context = {"cart_formset": cart_formset} context.update(extra_context or {}) settings.clear_cache() if settings.SHOP_DISCOUNT_FIELD_IN_CART and DiscountCode.objects.active( ).exists(): context["discount_form"] = discount_form return TemplateResponse(request, template, context)
def cart(request, template="shop/cart.html", cart_formset_class=CartItemFormSet, discount_form_class=DiscountForm, extra_context=None): """ Display cart and handle removing items from the cart. """ cart_formset = cart_formset_class(instance=request.cart) discount_form = discount_form_class(request, request.POST or None) if request.method == "POST": valid = True if request.POST.get("update_cart"): valid = request.cart.has_items() if not valid: # Session timed out. info(request, _("Your cart has expired")) else: cart_formset = cart_formset_class(request.POST, instance=request.cart) valid = cart_formset.is_valid() if valid: cart_formset.save() recalculate_cart(request) info(request, _("Cart updated")) else: # Reset the cart formset so that the cart # always indicates the correct quantities. # The user is shown their invalid quantity # via the error message, which we need to # copy over to the new formset here. errors = cart_formset._errors cart_formset = cart_formset_class(instance=request.cart) cart_formset._errors = errors else: valid = discount_form.is_valid() if valid: discount_form.set_discount() # Potentially need to set shipping if a discount code # was previously entered with free shipping, and then # another was entered (replacing the old) without # free shipping, *and* the user has already progressed # to the final checkout step, which they'd go straight # to when returning to checkout, bypassing billing and # shipping details step where shipping is normally set. recalculate_cart(request) if valid: return redirect("shop_cart") context = {"cart_formset": cart_formset} context.update(extra_context or {}) settings.clear_cache() if (settings.SHOP_DISCOUNT_FIELD_IN_CART and DiscountCode.objects.active().exists()): context["discount_form"] = discount_form return TemplateResponse(request, template, context)
def changelist_view(self, request, extra_context=None): if extra_context is None: extra_context = {} settings_form = SettingsForm(request.POST or None) if settings_form.is_valid(): settings_form.save() settings.clear_cache() info(request, _("Settings were successfully updated.")) return self.changelist_redirect() extra_context["settings_form"] = settings_form extra_context["title"] = "%s %s" % (_("Change"), force_text(Setting._meta.verbose_name_plural)) return super(SettingsAdmin, self).changelist_view(request, extra_context)
def changelist_view(self, request, extra_context=None): if extra_context is None: extra_context = {} settings_form = SettingsForm(request.POST or None) if settings_form.is_valid(): settings_form.save() settings.clear_cache() info(request, _("Settings were successfully updated.")) return self.changelist_redirect() extra_context["settings_form"] = settings_form extra_context["title"] = u"%s %s" % ( _("Change"), force_text(Setting._meta.verbose_name_plural)) return super(SettingsAdmin, self).changelist_view(request, extra_context)
def default_billship_handler(request, order_form): """ Default billing/shipping handler - called when the first step in the checkout process with billing/shipping address fields is submitted. Implement your own and specify the path to import it from via the setting ``SHOP_HANDLER_BILLING_SHIPPING``. This function will typically contain any shipping calculation where the shipping amount can then be set using the function ``cartridge.shop.utils.set_shipping``. The Cart object is also accessible via ``request.cart`` """ if not request.session.get("free_shipping"): settings.clear_cache() set_shipping(request, _("Flat rate shipping"), settings.SHOP_DEFAULT_SHIPPING_VALUE)
def test_editable_override(self): """ Test that an editable setting is always overridden by a settings.py setting of the same name. """ settings.clear_cache() Setting.objects.all().delete() django_settings.FOO = "Set in settings.py" Setting.objects.create(name="FOO", value="Set in database") first_value = settings.FOO settings.SITE_TITLE # Triggers access? second_value = settings.FOO self.assertEqual(first_value, second_value)
def test_invalid_value_warning(self): """ Test that a warning is raised when a database setting has an invalid value, i.e. one that can't be converted to the correct Python type. """ settings.clear_cache() register_setting(name="INVALID_INT_SETTING", editable=True, default=0) Setting.objects.create(name="INVALID_INT_SETTING", value="zero") with warnings.catch_warnings(): warning_re = r"The setting \w+ should be of type" warnings.filterwarnings("error", warning_re, UserWarning) with self.assertRaises(UserWarning): settings.INVALID_INT_SETTING self.assertEqual(settings.INVALID_INT_SETTING, 0)
def test_invalid_value_warning(self): """ Test that a warning is raised when a database setting has an invalid value, i.e. one that can't be converted to the correct Python type. """ settings.clear_cache() register_setting(name="INVALID_INT_SETTING", editable=True, default=0) Setting.objects.create(name="INVALID_INT_SETTING", value='zero') with warnings.catch_warnings(): warning_re = r'The setting \w+ should be of type' warnings.filterwarnings('error', warning_re, UserWarning) with self.assertRaises(UserWarning): settings.INVALID_INT_SETTING self.assertEqual(settings.INVALID_INT_SETTING, 0)
def category_processor(request, page): """ Add paging/sorting to the products for the category. """ settings.clear_cache() products = Product.objects.published(for_user=request.user).filter( page.category.filters()).distinct() sort_options = [(slugify(option[0]), option[1]) for option in settings.SHOP_PRODUCT_SORT_OPTIONS] sort_by = request.GET.get( "sort", sort_options[0][1] if sort_options else '-date_added') products = paginate(products.order_by(sort_by), request.GET.get("page", 1), settings.SHOP_PER_PAGE_CATEGORY, settings.MAX_PAGING_LINKS) products.sort_by = sort_by sub_categories = page.category.children.published() child_categories = Category.objects.filter(id__in=sub_categories) return {"products": products, "child_categories": child_categories}
def category_processor(request, page): """ Add paging/sorting to the products for the category. """ settings.clear_cache() products = Product.objects.published(for_user=request.user ).filter(page.category.filters()).distinct() sort_options = [(slugify(option[0]), option[1]) for option in settings.SHOP_PRODUCT_SORT_OPTIONS] sort_by = request.GET.get( "sort", sort_options[0][1] if sort_options else '-date_added') products = paginate(products.order_by(sort_by), request.GET.get("page", 1), settings.SHOP_PER_PAGE_CATEGORY, settings.MAX_PAGING_LINKS) products.sort_by = sort_by sub_categories = page.category.children.published() child_categories = Category.objects.filter(id__in=sub_categories) return {"products": products, "child_categories": child_categories}
def send_order_email(request, order): """ Send order receipt email on successful order. """ settings.clear_cache() order_context = { "order": order, "request": request, "order_items": order.items.all(), } order_context.update(order.details_as_dict()) receipt_template = "email/order_receipt" send_mail_template( settings.SHOP_ORDER_EMAIL_SUBJECT, receipt_template, settings.SHOP_ORDER_FROM_EMAIL, order.billing_detail_email, context=order_context, addr_bcc=settings.SHOP_ORDER_EMAIL_BCC or None, )
def send_order_email(request, order): """ Send order receipt email on successful order. """ settings.clear_cache() order_context = {"order": order, "request": request, "order_items": order.items.all()} order_context.update(order.details_as_dict()) try: get_template("shop/email/order_receipt.html") except TemplateDoesNotExist: receipt_template = "email/order_receipt" else: receipt_template = "shop/email/order_receipt" from warnings import warn warn("Shop email receipt templates have moved from " "templates/shop/email/ to templates/email/") send_mail_template(settings.SHOP_ORDER_EMAIL_SUBJECT, receipt_template, settings.SHOP_ORDER_FROM_EMAIL, order.billing_detail_email, context=order_context, addr_bcc=settings.SHOP_ORDER_EMAIL_BCC or None)
def test_unregistered_setting(self): """ Test that accessing any editable setting will delete all Settings with no corresponding registered setting from the database. """ settings.clear_cache() register_setting(name="REGISTERED_SETTING", editable=True, default="") Setting.objects.create(name="UNREGISTERED_SETTING", value="") with self.assertRaises(AttributeError): settings.UNREGISTERED_SETTING qs = Setting.objects.filter(name="UNREGISTERED_SETTING") self.assertEqual(qs.count(), 1) # This triggers Settings._load(), which deletes unregistered Settings settings.REGISTERED_SETTING self.assertEqual(qs.count(), 0)
def test_unregistered_setting(self): """ Test that accessing any editable setting will delete all Settings with no corresponding registered setting from the database. """ settings.clear_cache() register_setting(name="REGISTERED_SETTING", editable=True, default="") Setting.objects.create(name="UNREGISTERED_SETTING", value='') with self.assertRaises(AttributeError): settings.UNREGISTERED_SETTING qs = Setting.objects.filter(name="UNREGISTERED_SETTING") self.assertEqual(qs.count(), 1) # This triggers Settings._load(), which deletes unregistered Settings settings.REGISTERED_SETTING self.assertEqual(qs.count(), 0)
def test_conflicting_setting(self): """ Test that conflicting settings raise a warning and use the settings.py value instead of the value from the database. """ settings.clear_cache() register_setting(name="CONFLICTING_SETTING", editable=True, default=1) Setting.objects.create(name="CONFLICTING_SETTING", value=2) settings.CONFLICTING_SETTING = 3 with warnings.catch_warnings(): warning_re = "These settings are defined in both " "settings\.py and the database" warnings.filterwarnings("error", warning_re, UserWarning) with self.assertRaises(UserWarning): settings.CONFLICTING_SETTING self.assertEqual(settings.CONFLICTING_SETTING, 3) del settings.CONFLICTING_SETTING
def update_cart(request, template="shop/includes/offcanvas_cart.html", cart_formset_class=OffCartItemFormSet): """ Display cart and handle removing items from the cart. """ cart_formset = cart_formset_class(instance=request.cart) if request.is_ajax(): if request.POST and request.POST.get("update_cart"): valid = request.cart.has_items() if not valid: # Session timed out. info(request, _("Your cart has expired")) else: cart_formset = cart_formset_class(request.POST, instance=request.cart) valid = cart_formset.is_valid() if valid: cart_formset.save() info(request, _("Cart updated")) recalculate_cart(request) cart_formset = cart_formset_class(instance=request.cart) else: # Reset the cart formset so that the cart # always indicates the correct quantities. # The user is shown their invalid quantity # via the error message, which we need to # copy over to the new formset here. errors = cart_formset._errors cart_formset = cart_formset_class(instance=request.cart) cart_formset._errors = errors elif request.POST and request.POST.get("add_cart"): cart_formset = cart_formset_class(instance=request.cart) context = {"off_cart_formset": cart_formset} settings.clear_cache() return TemplateResponse(request, template, context) else: raise Http404
def test_conflicting_setting(self): """ Test that conflicting settings raise a warning and use the settings.py value instead of the value from the database. """ settings.clear_cache() register_setting(name="CONFLICTING_SETTING", editable=True, default=1) Setting.objects.create(name="CONFLICTING_SETTING", value=2) settings.CONFLICTING_SETTING = 3 with warnings.catch_warnings(): warning_re = ("These settings are defined in both " r"settings\.py and the database") warnings.filterwarnings("error", warning_re, UserWarning) with self.assertRaises(UserWarning): settings.CONFLICTING_SETTING self.assertEqual(settings.CONFLICTING_SETTING, 3) del settings.CONFLICTING_SETTING
def default_tax_handler(request, order_form): """ Default tax handler - called immediately after the handler defined by ``SHOP_HANDLER_BILLING_SHIPPING``. Implement your own and specify the path to import it from via the setting ``SHOP_HANDLER_TAX``. This function will typically contain any tax calculation where the tax amount can then be set using the function ``cartridge.shop.utils.set_tax``. The Cart object is also accessible via ``request.cart`` """ settings.clear_cache() if settings.SHOP_DEFAULT_TAX_RATE: locale.setlocale(locale.LC_NUMERIC, str(settings.SHOP_CURRENCY_LOCALE)) tax_rate = settings.SHOP_DEFAULT_TAX_RATE if settings.SHOP_TAX_INCLUDED: tax = request.cart.total_price() - ( request.cart.total_price() / decimal.Decimal(1 + tax_rate / 100)) tax_type = _("Incl.") + " " + f"{tax_rate:n}" + "% " + _("VAT") else: tax = request.cart.total_price() * decimal.Decimal(tax_rate / 100) tax_type = _("VAT") + " (" + f"{tax_rate:n}" + "%)" set_tax(request, tax_type, f"{tax:.2f}")
def __init__(self, request, step, data=None, initial=None, errors=None, **kwargs): """ Setup for each order form step which does a few things: - Calls OrderForm.preprocess on posted data - Sets up any custom checkout errors - Hides the discount code field if applicable - Hides sets of fields based on the checkout step - Sets year choices for cc expiry field based on current date """ # ``data`` is usually the POST attribute of a Request object, # which is an immutable QueryDict. We want to modify it, so we # need to make a copy. data = copy(data) # Force the specified step in the posted data, which is # required to allow moving backwards in steps. Also handle any # data pre-processing, which subclasses may override. if data is not None: data["step"] = step data = self.preprocess(data) if initial is not None: initial["step"] = step super(OrderForm, self).__init__(request, data=data, initial=initial, **kwargs) self._checkout_errors = errors # Hide discount code field if it shouldn't appear in checkout, # or if no discount codes are active. settings.clear_cache() if not (settings.SHOP_DISCOUNT_FIELD_IN_CHECKOUT and DiscountCode.objects.active().exists()): self.fields["discount_code"].widget = forms.HiddenInput() # Determine which sets of fields to hide for each checkout step. # A ``hidden_filter`` function is defined that's used for # filtering out the fields to hide. is_first_step = step == checkout.CHECKOUT_STEP_FIRST is_last_step = step == checkout.CHECKOUT_STEP_LAST is_payment_step = step == checkout.CHECKOUT_STEP_PAYMENT hidden_filter = lambda f: False if settings.SHOP_CHECKOUT_STEPS_SPLIT: if is_first_step: # Hide cc fields for billing/shipping if steps are split. hidden_filter = lambda f: f.startswith("card_") elif is_payment_step: # Hide non-cc fields for payment if steps are split. hidden_filter = lambda f: not f.startswith("card_") elif not settings.SHOP_PAYMENT_STEP_ENABLED: # Hide all cc fields if payment step is not enabled. hidden_filter = lambda f: f.startswith("card_") if settings.SHOP_CHECKOUT_STEPS_CONFIRMATION and is_last_step: # Hide all fields for the confirmation step. hidden_filter = lambda f: True for field in filter(hidden_filter, self.fields): self.fields[field].widget = forms.HiddenInput() self.fields[field].required = False # Set year choices for cc expiry, relative to the current year. year = now().year choices = make_choices(list(range(year, year + 21))) self.fields["card_expiry_year"].choices = choices
def __init__( self, request, step, data=None, initial=None, errors=None, **kwargs): """ Setup for each order form step which does a few things: - Calls OrderForm.preprocess on posted data - Sets up any custom checkout errors - Hides the discount code field if applicable - Hides sets of fields based on the checkout step - Sets year choices for cc expiry field based on current date """ # ``data`` is usually the POST attribute of a Request object, # which is an immutable QueryDict. We want to modify it, so we # need to make a copy. data = copy(data) # Force the specified step in the posted data, which is # required to allow moving backwards in steps. Also handle any # data pre-processing, which subclasses may override. if data is not None: data["step"] = step data = self.preprocess(data) if initial is not None: initial["step"] = step super(OrderForm, self).__init__( request, data=data, initial=initial, **kwargs) self._checkout_errors = errors # Hide discount code field if it shouldn't appear in checkout, # or if no discount codes are active. settings.clear_cache() if not (settings.SHOP_DISCOUNT_FIELD_IN_CHECKOUT and DiscountCode.objects.active().exists()): self.fields["discount_code"].widget = forms.HiddenInput() # Determine which sets of fields to hide for each checkout step. # A ``hidden_filter`` function is defined that's used for # filtering out the fields to hide. is_first_step = step == checkout.CHECKOUT_STEP_FIRST is_last_step = step == checkout.CHECKOUT_STEP_LAST is_payment_step = step == checkout.CHECKOUT_STEP_PAYMENT hidden_filter = lambda f: False if settings.SHOP_CHECKOUT_STEPS_SPLIT: if is_first_step: # Hide cc fields for billing/shipping if steps are split. hidden_filter = lambda f: f.startswith("card_") elif is_payment_step: # Hide non-cc fields for payment if steps are split. hidden_filter = lambda f: not f.startswith("card_") elif not settings.SHOP_PAYMENT_STEP_ENABLED: # Hide all cc fields if payment step is not enabled. hidden_filter = lambda f: f.startswith("card_") if settings.SHOP_CHECKOUT_STEPS_CONFIRMATION and is_last_step: # Hide all fields for the confirmation step. hidden_filter = lambda f: True for field in filter(hidden_filter, self.fields): self.fields[field].widget = forms.HiddenInput() self.fields[field].required = False # Set year choices for cc expiry, relative to the current year. year = now().year choices = make_choices(list(range(year, year + 21))) self.fields["card_expiry_year"].choices = choices