First: Homework Questions

Continuing the HuskyShop App

Add Cart

A couple of design questions:

So each user has one cart, which has many cart items.

$ mix phx.gen.html Carts CartItem cart_items user_id:references:users product_id:references:products count:integer

Before we run the migration, let’s edit it:


  create table(:cart_items) do
    add :count, :integer, null: false
    add :user_id, references(:users, on_delete: :delete_all), null: false
    add :product_id, references(:products, on_delete: :delete_all), null: false


Add an “add to cart” form to product page.

Copy form from cart item and modify into:


<%= if @current_user do %>
    <%= form_for @item_cset, Routes.cart_item_path(@conn, :create),
               [class: "form-inline"], fn f -> %>

      <%= hidden_input f, :user_id %>
      <%= hidden_input f, :product_id %>

      <div class="form-group">
        <%= number_input f, :count, class: "form-control" %>
        <%= error_tag f, :count %>

        <%= submit "Add to Cart", class: "btn btn-primary" %>
    <% end %>
<% end %>

Make a changeset for the cart item in product_controller

alias HuskyShop.Carts
  def show(conn, %{"id" => id}) do
    product = Products.get_product!(id)
    user_id = get_session(conn, :user_id)
    item_cset = Carts.change_cart_item(%Carts.CartItem{
          user_id: user_id, product_id:, count: 1})
    render(conn, "show.html", product: product, item_cset: item_cset)

Add a cart column to the main layout.

      <div class="row">
        <div class="col-8">
          <%= render @view_module, @view_template, assigns %>

        <div class="col-4">
          <%= if @current_user do %>
            <%= render HuskyShopWeb.CartItemView, "index.html", 
                  cart_items: @current_user.cart_items %>
          <% else %>
            <p><%= link "Register", to: Routes.user_path(@conn, :new) %></p>
          <% end %>

Users don’t have a cart items field. We need to tell Ecto about the relationship between users and cart items to fix this.


schema "users" do
  has_many :cart_items, HuskyShop.Carts.CartItem

Add to cart button doesn’t work, and we want each cart item to belong to a specific user and product.


  schema "cart_items" do
    field :count, :integer
    belongs_to :user, HuskyShop.Users.User
    belongs_to :product, HuskyShop.Products.Product


  @doc false
  def changeset(cart_item, attrs) do
    |> cast(attrs, [:count, :user_id, :product_id])
    |> validate_required([:count, :user_id, :product_id])

We should probably show item names in the cart.


Update the preload to be two deep.


def get_user(id) do
  preload: [cart_items: :product]

Lets add the last relationship. For any product, we can see what carts it’s in.


schema ...
  has_many :cart_items, HuskyShop.Carts.CartItem

Let’s take a look at this preload thing.

(make sure user 2 has some cart items first)

$ iex -S mix
iex> alias HuskyShop.Repo
iex> import Ecto.Query
iex> alias HuskyShop.Users.User
iex> alias HuskyShop.Products.Product
iex> (from u in User, limit: 1) |>
iex> (from u in User, where: == 2) |> preload([cart_items: :product]) |> 
iex> (from p in Product) |> preload(:cart_items) |> Repo.all

Core Ecto relationships:

See the Ecto Schema documentation for more details and examples.

(current state is branch: 4-cart)


Prevent non-admin users from managing products:

Create a new plug in plugs/require_admin.ex

defmodule HuskyShopWeb.Plugs.RequireAdmin do
  use HuskyShopWeb, :controller

  def init(args), do: args

  def call(conn, _params) do
    user = conn.assigns[:current_user]
    if user.admin do
      |> put_flash(:error, "You can't do that.")
      |> redirect(to: Routes.page_path(conn, :index))
      |> halt

In products_controller, add:

plug HuskyShopWeb.Plugs.RequireAdmin when action in [:new, :create, :update, :delete]

Only show product new / edit / delete buttons if user is admin.

Make the shopping cart link to the item.

In cart_item/index:

<!-- item name -->
  <td><%= link(,
    to: Routes.product_path(@conn, :show, cart_item.product_id)) %></td>
<!-- remove show and edit links -->

Update redirects for cart_items to always redirect to the product.

In cart_item_controller:

  |> redirect(to: Routes.product_path(conn, :show, cart_item.product_id))

More cleanups:

Add filler image:

Replace product list with cards:

In lib/husky_shop_web/templates/product/index.html.eex

Replace the table markup with:

<div class="row">
  <%= for product <- @products do %>
    <div class="card col-6">
      <%= link(
        img_tag(Routes.static_path(@conn, "/images/rubber-duck.png"), 
          class: "card-img-top"),
        to: Routes.product_path(@conn, :show, product)) %>

      <div class="card-body">
        <div class="card-title">
          <%= link(, to: Routes.product_path(@conn, :show, product)) %>

        <div class="card-text">
          <p><%= product.desc %></p>

          <% if @current_user.admin do %>
              <%= link("Edit", to: Routes.product_path(@conn, :edit, product),
                class: "btn btn-info") %>
              <%= link("Delete", to: Routes.product_path(@conn, :delete, product),
                method: :delete, data: [confirm: "Are you sure?"],
                class: "btn btn-danger") %>
          <% end %>
  <% end %>

(current state: 5-cleanup)

Closing Notes

The key thing for the next few assignments is learning and using what these libraries give you. To do that: