mic
mic

Reputation: 1266

Store extra information for product before adding to cart

tl;dr How do I change the add-product-to-cart-footer to add an instance of a different model that's a combination of that product and other information?

I'm selling glasses, so while we have the product detail page showing the frame and frame variant (varying by color and size) information, the customer needs to also fill out a form for what type of lenses they want to get with that frame.

One option could be to store each permutation of a frame variant and lenses combination as a frame variant in the database, like different storage options for smartphones, but I don't think that would be a good idea because:

Instead, I'm thinking of having a Glasses model with foreign keys to a frame variant and lenses, and this is what would be added to the cart.

class Lenses(models.Model):
    material = models.CharField(choices=...)
    anti_reflective = models.CharField(choices=...)
    # ...

class Glasses(models.Model):
    frame = models.ForeignKey(FrameVariant)
    lenses = models.ForeignKey(Lenses)

I'd like to modify the "Add to cart" button so that instead of adding the frame variant to the cart, it goes to a form (possibly overlaid on the same page) to choose the lenses. The relevant block to override is add-product-to-cart-footer from shop/catalog/product-add2cart:

{% block add-product-to-cart-footer %}
  <div class="card-footer bg-white">
    <div class="d-flex flex-column flex-lg-row justify-content-between">
      <button class="btn btn-secondary m-1" ng-disabled="context.is_in_cart" ng-click="do(addToCart('{% url "shop:watch-list" %}'))">
        {% trans "Watch product" %}
        <i class="fa" ng-class="context.is_in_cart ? 'fa-heart' : 'fa-heart-o'"></i>
      </button>
      {% url "shop:cart-list" as cart_endpoint %}{% trans "The product has been successfully placed in the shopping cart:" as modal_title %}
      <button
        class="btn btn-primary m-1"
        ng-disabled="!context.availability.quantity"
        ng-click="
          {% if use_modal_dialog %}
            do(openModal('{{ modal_title }}'))
              .then(addToCart('{{ cart_endpoint }}'))
              .then(redirectTo())
          {% else %}
            do(addToCart('{{ cart_endpoint }}'))
              .then(emit('shop.cart.change'))
          {% endif %}
        "
      >
        {% trans "Add to cart" %}
        <i class="fa fa-cart-arrow-down"></i>
      </button>
    </div>
    {% if request.session.is_empty %}
      <small class="text-muted m-1">{% block cookie-disclaimer %}
        {% blocktrans %}By adding a product to the cart you are giving consent to cookies being used.{% endblocktrans %}
      {% endblock %}</small>
    {% endif %}
  </div>
{% endblock add-product-to-cart-footer %}

addToCart(endpoint) from shop/static/shop/js/catalog.js:

scope.addToCart = function(endpoint) {
	return function(context) {
		var deferred = $q.defer();
		$http.post(endpoint, scope.context).then(function(response) {
			scope.context.is_in_cart = true;
			deferred.resolve(context);
		}).catch(function(response) {
			$log.error('Unable to update context: ' + response.statusText);
			deferred.reject();
		});
		return deferred.promise;
	};
};

It looks like context is what determines what's added to the cart, and so that's what I'd have to change. It seems like context might be being passed by shop/cascade/catalog.py:

class ShopAddToCartPlugin(ShopPluginBase):
    name = _("Add Product to Cart")
    require_parent = True
    parent_classes = ('BootstrapColumnPlugin',)
    cache = False

    use_modal_dialog = GlossaryField(
        widgets.CheckboxInput(),
        label=_("Use Modal Dialog"),
        initial='on',
        help_text=_("After adding product to cart, render a modal dialog"),
    )

    def get_render_template(self, context, instance, placeholder):
        templates = []
        if instance.glossary.get('render_template'):
            templates.append(instance.glossary['render_template'])
        if context['product'].managed_availability():
            template_prefix = 'available-'
        else:
            template_prefix = ''
        templates.extend([
            '{}/catalog/{}product-add2cart.html'.format(app_settings.APP_LABEL, template_prefix),
            'shop/catalog/{}product-add2cart.html'.format(template_prefix),
        ])
        return select_template(templates)

    def render(self, context, instance, placeholder):
        context = super(ShopAddToCartPlugin, self).render(context, instance, placeholder)
        context['use_modal_dialog'] = bool(instance.glossary.get('use_modal_dialog', True))
        return context

Looking up ShopAddToCartPlugin didn't turn up any notable usages either in the Django Shop code or in what was produced by Cookiecutter, so I'm not sure what sort of context would be passed to render.

EDIT: Another option is adding the frame variant to the cart as usual, but redirecting to a lens form which creates a Lenses object that's associated with that frame variant. That seems like it could be simpler.

To help with visualizing the template, here's what Django Shop looks like: Django Shop product detail screenshot

Upvotes: 0

Views: 116

Answers (1)

jrief
jrief

Reputation: 1695

In a similar use-case, I kept the products as separate items in the cart. This means, that I would not use a foreign key from Glasses to Lenses.

Instead, I would create a special AddGlassCartSerializer(...), which examines the supplied (posted) data. There I would distinguish between the frame, the left and the right lens. You might even have to keep track of the lenses orientation, in case you offer anisotropic variants.

Then, after adding a glass into the cart, you always get 3 items: The frame, the left and the right lens.

Upvotes: 1

Related Questions