Rails Bitcoin Prodavnica - deo I
Tags: ruby, rails, webdev, blockchain
Published: 2020-12-11 13:19:00

This is tutorial in Serbian language, creating simple shop with Rails 6 and Bitcoin, without JavaScript (or to not depends on JavaScript).

Prvi u seriji od 3 tutoriala u kojima cemo napraviti kompletnu prodavnicu, bez funkcija koje necemo koristiti, bez online wallet-a, privatnih kljuceva. Siguran i jednostavan nacin, koristeci **BIP32 Master Public Key.** Podrazumeva se osnovno znanje *Ruby on Rails* i *MVC.*

U prvom delu napravicemo novu aplikaciju, korisnike, proizvode i kopru. U drugom delu porudzbine i placanje, i u trecem delu izgled sajta, *pagination*, i priprema za *production env.* 

---

## Nova aplikacija

---

Kreiraj novu rails aplikaciju (istraziti *rails new* komandu, kao i --no-javascript i --api opcije).

`rails new example-shop`

Otvorite Gemfile i u njega dodajte sledece:

```ruby
gem 'devise'
gem 'bulma'
```  

Ovo ce instalirati gemove za autentikaciju (devise) i za stil (bulma).
Nakon toga kucajte `bundle install` za instaliranje gemova.

---

## Novi Korisnik

---

```bash
rails g devise:install
rails g devise user
rails g controller registrations
```

Ovo ce integrisati devise u vasu aplikaciju, dodati model i tabelu *:users*,
i napraviti novi kontroler za registracije. Otvorite *Registrations Controller:*

```ruby
class RegistrationsController < Devise::RegistrationsController

 private

 def sign_up_params
  params.require(:user).permit(:name, :address, :country, :email, :password, :password_confirmation)
 end

 def account_update_params
  params.require(:user).permit(:name, :address, :country, :email, :password, :password_confirmation, :current_password)
 end

end
```

Registrations Controller proizilazi iz Devise::Registrations, pa u njemu menjamo *params*, da bi smo
dodali nova polja (:name, :address, :country). Nakon toga napravite novu migraciju:

`rails g migration add_fields_to_users`

```ruby
class AddFieldsToUsers < ActiveRecord::Migration[6.0]

  def change
    add_column :users, :name,    :string, null: false, default: ""
    add_column :users, :country, :string, null: false, default: ""
    add_column :users, :address, :text
  end

end
```  
Za kraj izvrsite komandu `rails db:migrate`.

Otvoride model User i dodajte sledece:

```ruby
class User < ApplicationRecord

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable

  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  validates :name,    length: { maximum:  16, too_long: "%{count} chars is maximum for User name."}
  validates :country, length: { maximum:  20, too_long: "%{count} chars is maximum for User country."}
  validates :address, length: { maximum:  75, too_long: "%{count} chars is maximum for User address."}

  has_many :products

end
```
Provera da li je neko polje validno moze da se definise u modelu ili direktno u DB (migrations).
Ako je polje obavezno za sve, definisemo *null: false*, ali nekad je polje obavezno za jednu grupu
korisnika, dok za drugu nije. Zato sve provere treba definisati u modelu.

---

## Dodati Proizvode

---

Rails *scaffold* automatski napravi model, kontroler, kao i *views*.

`rails g scaffold product title description:text photo price:decimal user_id:integer published:boolean`

Otvorite ProductsController i promenite kod da izgleda ovako:

```ruby
class ProductsController < ApplicationController


  before_action :authenticate_user!, except: [:show, :index]
  before_action :set_product,        only:   [:show, :update, :destroy, :edit]


  def index
    @products = Product.all
  end

  def show
  end

  def new
    @product = current_user.products.build
  end

  def edit
  end


  def create
    @product = current_user.products.build(product_params)
    if @product.save
      redirect_to @product, notice: "Product #{@product.title} has been created."
    else
      render :new
    end
  end


  def update
    if @product.update(product_params)
      redirect_to @product, notice: "Product #{@product.title} has been updated."
    else
      render :edit
    end
  end

  def destroy
    @product.published = false if @product.user_id == current_user.id
    redirect_to root_path, notice: 'Product was successfully destroyed.'
  end


  private

  def set_product
    @product = Product.find(params[:id])
  end

  def product_params
    params.require(:product).permit(:title, :price, :description, :photo, :user_id, :published)
  end

end
```

Promenite model Product da izleda:

```ruby
class Product < ApplicationRecord

  belongs_to :user

  validates :title, :price, :description, :photo, :user_id, :published, presence: true
  validates :price,         length: { maximum:    7 }
  validates :title,         length: { maximum:  140, too_long: "Title is too long! [%{count}] chars is maximum."}
  validates :description,   length: { maximum: 1000, too_long: "Description is too long! [%{count}] chars is maximum."}

end
```

Na kraju migracije:

```ruby
class CreateProducts < ActiveRecord::Migration[6.0]
  def change
    create_table :products do |t|
      t.string  :title,       null: false, default: ""
      t.decimal :price,       null: false, default: 0
      t.text    :description
      t.string  :photo
      t.boolean :published,   null:false, default: true

      t.timestamps
    end
  end
end
```
Izvrsite `rails db:migrate` i kreirali ste *Products* za vasu aplikaciju!

---

## Shopping Cart

---

Shopping Cart je nezaobilazni deo svake prodavnice. Za to su nam potrebni
**Cart** i **CartItem**. Ponovo koristimo *scaffold*, i pocinjemo sa **Cart** modelom.

`rails g scaffold cart`

Otvorite CartsController i neka izgleda ovako:

```ruby
class CartsController < ApplicationController

  rescue_from   ActiveRecord::RecordNotFound, with: :invalid_cart

  before_action :set_cart, except: [:create, :new]


  def index
    @carts = list_cart_products
  end

  def new
    @cart = current_user.carts.build
  end

  def create
    @cart = current_user.carts.build(cart_params)
    if @cart.save
      redirect_to @cart
    else
      render :new
    end
  end

  def destroy
    if current_cart.id == session[:cart_id]
      @cart.destroy
      session[:cart_id] = session[:token] = nil
      set_cart and redirect_to root_url, notice: 'Your cart is empty!'
    end
  end


  private

  def list_cart_products
    CartItem.where(cart_id: session[:cart_id]).all
  end

  def invalid_cart
    logger.error "Attempt to access invalid cart with ID: #{params[:id]}"
    redirect_to root_path, notice: "Cart doesn't exist!"
  end

  def cart_params
    params.require(:cart).permit(:token)
  end

end
```

Kako bi odrzali korpu i za korisnike koji nisu registrovani, koristimo *:sessions*.
Takodje koristimo i *:token*, koji ce kasnije sluziti za *friendly-id gem* kao rute,
i svaki model verifikuje token (cart, order, payment)

Sada otvorite Cart model i dodajte sledece:

```ruby
class Cart < ApplicationRecord

  has_many  :cart_items, dependent: :destroy

  validates :token, presence: true


  def add(product)
    cart_item = cart_items.find_by(product_id: product.id)
    cart_item ? cart_item.quantity += 1 : cart_item = cart_items.build(product_id: product.id)
  end


  def total_price
    cart_items.to_a.sum { |item| item.total_price }
  end

end

```

Otvorite *concerns* u folderu *models* i napravite **shopping_cart.rb**.
U njega dodajte sledece:

```ruby
module ShoppingCart

  private

  def set_cart
    begin
      @cart = Cart.find(session[:cart_id])
    rescue    ActiveRecord::RecordNotFound
      @cart = Cart.create(token: generate_token)
      session[:token]   = @cart.token
      session[:cart_id] = @cart.id
    end
  end

  def generate_token
    SecureRandom.urlsafe_base64
  end

end
```

Otvorite ApplicationController i dodajte sledece:

```ruby
class ApplicationController < ActionController::Base

  protect_from_forgery with: :exception

  include ShoppingCart

  before_action :set_cart
  helper        :all

  def current_cart
    Cart.find(session[:cart_id])
  end

end
```

Sada napravite **CartItem**, ponovo uz pomoc scaffold-a:

`rails g scaffold cart_item`

Otvorite CartItems Controller i dodajte sledece:

```ruby
class CartItemsController < ApplicationController

  before_action :set_cart
  before_action :set_cart_item, except: [:index, :create]

  def index
    @cart_item = CartItem.find_by(cart_id: session[:cart_id])
  end

  def show
    @cart_item = CartItem.find(params[:id])
  end

  def new
    @cart_item = CartItem.new
  end

  def create

    @product              = Product.find(params[:product_id])
    @cart_item            = @cart.add(@product)
    @cart_item.product_id = @product.id

    if @cart_item.save
      redirect_to carts_path, notice: "#{@cart_item.product.title} added to cart for $#{@cart_item.total_price}"
    else
      render :new
    end
  end


  def destroy
    if @cart_item.cart_id == @cart.id
      @cart.destroy
      set_cart and redirect_to @cart, notice: "#{@cart_item.product.title} has been removed from cart!"
    end
  end


  private

  def set_cart_item
    @cart_item = CartItem.find_by(id: params[:id], cart_id: @cart.id)
  end

  def cart_item_params
    params.require(:cart_item).permit(:product_id, :cart_id, :quantity, :total_price)
  end

end
```

Sada otvorite model CartItem i dodajte:

```ruby
class CartItem < ApplicationRecord

  belongs_to :cart
  belongs_to :product

  def total_price
    product.price.to_i * quantity.to_i
  end

end
```

I to je sve za prvi deo! Sada imamo potpuno funkcionalne korisnike, proizvode i korpu.
U drugom delu dodacemo porudzbine i placanje Bitcoinom, kao i izgled sajta koristeci *bulmu* ili *bootstrap*, po vasem izboru.

Ostavite *wordpress* i *woo-commerce*, korstite *Ruby on Rails* za online prodavnicu.

---

[=> Procitajte Deo II](https://dev.to/linuxander/rails-bitcoin-prodavnica-deo-ii-2ilj)