Commit 92e3a257 by Trong Huu Nguyen

Merge branch 'dhp_user_management' into 'development'

[REVIEW] User management

See merge request !2
parents 7099ab3b 68223ff6
......@@ -17,3 +17,9 @@
/yarn-error.log
.byebug_history
# Ignore public/uploads folder
/public/uploads/*
# Ignore database config file
/config/database.yml
\ No newline at end of file
......@@ -40,6 +40,10 @@ gem 'bootstrap-sass', '~> 3.3.6'
gem 'faker', '~> 1.6', '>= 1.6.3'
# Kaminari is a Scope & Engine based, clean, powerful, agnostic, customizable and sophisticated paginator for Rails 4+
gem 'kaminari', '~> 1.0', '>= 1.0.1'
# Flexible authentication solution for Rails with Warden
gem 'devise', '~> 4.3'
# Automatic Ruby code style checking tool. Aims to enforce the community-driven Ruby Style Guide.
gem 'rubocop', '~> 0.49.1'
# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development
......
......@@ -41,8 +41,10 @@ GEM
addressable (2.5.1)
public_suffix (~> 2.0, >= 2.0.2)
arel (8.0.0)
ast (2.3.0)
autoprefixer-rails (7.1.1)
execjs
bcrypt (3.1.11)
bindex (0.5.0)
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
......@@ -70,6 +72,12 @@ GEM
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.0.5)
devise (4.3.0)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0, < 5.2)
responders
warden (~> 1.2.3)
erubi (1.6.0)
execjs (2.7.0)
faker (1.6.6)
......@@ -113,6 +121,11 @@ GEM
nio4r (2.1.0)
nokogiri (1.8.0)
mini_portile2 (~> 2.2.0)
orm_adapter (0.5.0)
parallel (1.11.2)
parser (2.4.0.0)
ast (~> 2.2)
powerpack (0.1.1)
public_suffix (2.0.5)
puma (3.9.1)
rack (2.0.3)
......@@ -141,10 +154,23 @@ GEM
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
rake (12.0.0)
rb-fsevent (0.9.8)
rb-inotify (0.9.8)
ffi (>= 0.5.0)
responders (2.4.0)
actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3)
rubocop (0.49.1)
parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-progressbar (1.8.1)
ruby_dep (1.5.0)
rubyzip (1.2.1)
sass (3.4.24)
......@@ -180,6 +206,9 @@ GEM
thread_safe (~> 0.1)
uglifier (3.2.0)
execjs (>= 0.3.0, < 3)
unicode-display_width (1.2.1)
warden (1.2.7)
rack (>= 1.0)
web-console (3.5.1)
actionview (>= 5.0)
activemodel (>= 5.0)
......@@ -201,6 +230,7 @@ DEPENDENCIES
capybara (~> 2.13)
carrierwave (~> 1.0)
coffee-rails (~> 4.2)
devise (~> 4.3)
faker (~> 1.6, >= 1.6.3)
jbuilder (~> 2.5)
kaminari (~> 1.0, >= 1.0.1)
......@@ -209,6 +239,7 @@ DEPENDENCIES
mysql2 (>= 0.3.18, < 0.5)
puma (~> 3.7)
rails (~> 5.1.1)
rubocop (~> 0.49.1)
sass-rails (~> 5.0)
selenium-webdriver
spring
......
......@@ -12,4 +12,4 @@
//
//= require rails-ujs
//= require turbolinks
//= require_tree .
//# require_tree .
......@@ -10,6 +10,9 @@
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*# require_tree .
*= require_self
*= require custom
*= require main_style
*= require skin_14
*/
body {
font-size: 13px;
line-height: 1.5;
background: image-url('shop-14-bg.png') top center repeat-x;
}
a {
color: #000000;
}
a:hover {
color: #0d0d0d;
}
a:focus {
color: #0d0d0d;
}
a:active {
color: #000000;
}
.pagination > li > a,
.pagination > li > span,
.pagination > li > a:hover,
.pagination > li > span:hover,
.pagination > li > a:focus,
.pagination > li > span:focus {
color: #000000;
}
.pagination > .active > a,
.pagination > .active > span,
.pagination > .active > a:hover,
.pagination > .active > span:hover,
.pagination > .active > a:focus,
.pagination > .active > span:focus {
background-color: #000000 !important;
border-color: #000000;
}
html .btn-primary {
color: #ffffff;
background-color: #000000;
border-color: #000000 #000000 #000000;
}
html .btn-primary:hover {
border-color: #0d0d0d #0d0d0d #000000;
background-color: #0d0d0d;
}
html .btn-primary:active,
html .btn-primary:focus,
html .btn-primary:active:hover,
html .btn-primary:active:focus {
border-color: #000000 #000000 #000000;
background-color: #000000;
}
html .btn-primary.dropdown-toggle {
border-left-color: #000000;
}
html .btn-primary[disabled],
html .btn-primary[disabled]:hover,
html .btn-primary[disabled]:active,
html .btn-primary[disabled]:focus {
border-color: #333333;
background-color: #333333;
}
html .btn-primary:hover,
html .btn-primary:focus,
html .btn-primary:active:hover,
html .btn-primary:active:focus {
color: #ffffff;
}
.btn {
border-radius: 0;
}
.featured-box .box-content {
border-radius: 0;
}
.product-essential {
margin-bottom: 50px;
}
@media (max-width: 767px) {
.product-img-box {
margin-bottom: 25px;
}
}
.product-img-box img {
display: block;
width: 100%;
height: auto;
}
.product-img-box .product-img-wrapper {
padding: 3px;
border: 1px solid #ddd;
border-radius: 0;
}
.product-img-box-wrapper {
position: relative;
margin-bottom: 10px;
}
.product-details-box .product-name {
margin: 15px 0;
font-size: 28px;
font-weight: 600;
line-height: 1;
color: #555;
}
.product-details-box .product-short-desc {
padding: 0 0 10px;
border-bottom: 1px solid #ebebeb;
}
.product-details-box .product-short-desc p {
font-size: 14px;
line-height: 1.65;
margin: 0 0 20px;
}
.product-details-box .product-detail-info {
padding-bottom: 20px;
margin-top: 20px;
border-bottom: 1px solid #ebebeb;
}
.product-details-box .product-detail-info .product-price-box {
margin: 0 0 20px;
}
.product-details-box .product-detail-info .product-price-box .product-price {
font-size: 33px;
line-height: 1;
color: #000;
}
#header .header-body {
border-top: none;
background-color: transparent;
border-bottom: none;
padding: 0;
}
@media (min-width: 992px) {
#header .header-body {
padding-bottom: 15px;
}
}
#header .header-logo img {
margin: 0 24px 0 0;
}
#header .header-container {
padding-top: 28px;
padding-bottom: 28px;
}
#header .header-container.header-nav {
padding: 0;
}
#header .cart-area {
float: right;
vertical-align: middle;
}
@media (max-width: 991px) {
#header .cart-area {
margin-top: 5.5px;
}
}
header .header-search {
position: relative;
float: right;
margin: 0;
font-size: 0;
line-height: 1;
padding: 0;
border: none;
}
@media (min-width: 992px) {
#header .header-search {
float: left;
}
}
@media (max-width: 991px) {
#header .header-search {
margin-top: 5.5px;
margin-left: 5px;
margin-right: 5px;
}
}
@media (max-width: 360px) {
#header .header-search {
margin-left: 0;
margin-right: 0;
}
}
#header .header-search .search-toggle {
display: inline-block;
font-size: 14px;
line-height: 40px;
min-width: 25px;
text-align: center;
}
@media (min-width: 992px) {
#header .header-search .search-toggle {
display: none;
}
}
#header .header-search form {
display: inline-block;
width: 0;
}
@media (min-width: 992px) {
#header .header-search form {
width: 400px;
}
}
@media (min-width: 992px) {
#header .header-search form {
width: 450px;
}
}
#header .header-search .header-search-wrapper {
display: none;
overflow: visible;
border: 1px solid #ccc;
border-radius: 0;
position: relative;
width: 100%;
min-width: 250px;
padding-right: 170px;
background-color: #fff;
}
#header .header-search .header-search-wrapper.open {
display: block;
}
#header .header-search .header-search-wrapper:after {
content: '';
display: table;
clear: both;
}
@media (min-width: 992px) {
#header .header-search .header-search-wrapper {
display: block;
}
}
@media (max-width: 991px) {
#header .header-search .header-search-wrapper {
position: absolute;
right: -50px;
top: 100%;
border-width: 5px;
width: 450px;
border-radius: 0;
}
#header .header-search .header-search-wrapper:before {
content: "";
display: block;
position: absolute;
right: 45px;
top: -25px;
width: 20px;
height: 20px;
border: 10px solid transparent;
border-bottom-color: #ccc;
}
}
@media (max-width: 480px) {
#header .header-search .header-search-wrapper {
width: 300px;
}
}
@media (max-width: 350px) {
#header .header-search .header-search-wrapper {
width: 240px;
}
}
#header .header-search .header-search-wrapper .form-control,
#header .header-search .header-search-wrapper select {
float: left;
height: 38px;
font-family: Arial;
font-size: 13px;
background-color: #fff;
margin: 0;
}
#header .header-search .header-search-wrapper .form-control {
padding: 9px 15px;
color: #999;
width: 100%;
margin: 0;
line-height: 20px;
border-radius: 0 0 0 0;
box-shadow: none;
border: none;
}
#header .header-search .header-search-wrapper select {
position: absolute;
right: 40px;
width: 130px;
border: 1px solid #ccc;
border-top: 0;
border-bottom: 0;
line-height: 36px;
color: #777;
padding: 2px 0;
padding-left: 10px;
border-radius: 0;
-moz-appearance: none;
-webkit-appearance: none;
}
@media (max-width: 350px) {
#header .header-search .header-search-wrapper select {
width: 110px;
}
}
#header .header-search .header-search-wrapper .btn.btn-default {
position: absolute;
left: auto;
right: 0;
top: 0;
width: 40px;
height: 38px;
color: #777;
background-color: transparent;
font-size: 14px;
border: 0;
padding: 0;
margin: 0;
background: transparent;
cursor: pointer;
border-radius: 0 0 0 0;
}
#header .header-search .header-search-wrapper .btn.btn-default:hover, #header .header-search .header-search-wrapper .btn.btn-default:focus {
color: #000;
background-color: transparent;
}
.panel-default>.panel-heading {
color: #333;
background-color: #f5f5f5;
border-color: #ddd;
}
.sidebar.shop-sidebar .panel-group {
margin-bottom: 40px;
}
.sidebar.shop-sidebar .panel-group .panel + .panel {
margin-top: 14px;
}
.sidebar.shop-sidebar .panel.panel-default {
border-radius: 0;
border: none;
overflow: hidden;
}
.sidebar.shop-sidebar .panel.panel-default .panel-heading {
border-radius: 0;
}
.sidebar.shop-sidebar .panel.panel-default .panel-heading .panel-title {
margin: 0;
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
color: #777;
}
.sidebar.shop-sidebar .panel.panel-default .panel-heading a {
position: relative;
border-radius: 0 0 0 0;
padding-top: 11.5px;
padding-bottom: 11.5px;
padding-right: 45px;
color: #777;
border: 1px solid #ddd;
}
.sidebar.shop-sidebar .panel.panel-default .panel-heading a:before {
font-family: 'FontAwesome';
content: "\f0d8";
width: 26px;
height: 26px;
display: block;
border: 1px solid #ddd;
position: absolute;
right: 15px;
top: 50%;
margin-top: -13px;
border-radius: 0;
color: #ccc;
text-align: center;
line-height: 24px;
background-color: #fff;
font-size: 10px;
}
.sidebar.shop-sidebar .panel.panel-default .panel-heading a:hover:before {
background-color: #000;
border-color: #000;
color: #fff;
}
.sidebar.shop-sidebar .panel.panel-default .panel-heading a.collapsed {
border-radius: 0;
}
.sidebar.shop-sidebar .panel.panel-default .panel-heading a.collapsed:before {
content: "\f0d7";
}
.sidebar.shop-sidebar .panel.panel-default .panel-body {
padding: 15px 15px 7px;
background-color: #fbfbfb;
}
#footer {
border-top: none;
background-color: #54555e;
color: #bbbbbb;
font-size: 13px;
padding-top: 0;
}
#footer .footer-copyright {
color: #bbb;
background-color: #45464e;
border-top: none;
padding: 29.5px 0;
margin-top: 0;
}
@media (min-width: 992px) {
#footer .footer-copyright .logo,
#footer .footer-copyright .social-icons,
#footer .footer-copyright .footer-payment {
float: left;
margin-bottom: 0;
}
#footer .footer-copyright .logo {
margin-right: 45px;
}
#footer .footer-copyright .social-icons {
margin-right: 60px;
}
#footer .footer-copyright .social-icons li {
margin-bottom: 0;
}
#footer .footer-copyright .footer-payment {
margin-top: 1px;
}
#footer .footer-copyright .copyright-text {
float: right;
margin-bottom: 0;
margin-top: 6px;
}
}
@media (max-width: 991px) {
#footer .footer-copyright {
text-align: center;
}
#footer .footer-copyright .logo {
margin-bottom: 8px;
}
#footer .footer-copyright .social-icons {
margin-bottom: 5px;
}
#footer .footer-copyright .logo img,
#footer .footer-copyright .footer-payment {
margin-left: auto;
margin-right: auto;
}
#footer .footer-copyright .footer-payment {
margin-bottom: 10px;
}
}
\ No newline at end of file
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
# rescue_from ActiveRecord::RecordNotFound, :with => :render_404
def render_404
respond_to do |format|
format.html { render file: "#{Rails.root}/public/404", layout: false, status: :not_found }
format.xml { head :not_found }
format.any { head :not_found }
end
end
end
class ProductsController < ApplicationController
before_action :authenticate_user!, only: [:new, :edit, :create, :update, :destroy]
before_action :set_product, only: :show
before_action :user_can_edit_product, only: [:edit, :update, :destroy]
# GET /products/new
def new
@product = Product.new
end
# POST /products
def create
@product = Product.new(product_params.merge(user_id: current_user.id))
if @product.save
redirect_to root_url, flash: { success: "Product #{@product.title} is sucessfully created" }
else
render 'new'
end
end
# PATCH/PUT /products/1
def update
if @product.update(product_params)
redirect_to root_url, flash: { success: "Product #{@product.title} is sucessfully updated" }
else
render 'edit'
end
end
# DELETE /products/1
def destroy
if @product.destroy
flash[:success] = "Product #{@product.title} deleted"
else
flash[:alert] = "Product #{@product.title} can't be deleted"
end
redirect_to root_url
end
private
# Use callbacks to share common setup or constraints between actions.
def set_product
@product = Product.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def product_params
params.require(:product).permit(:title, :sku, :price, :description,
:category_id, :image_url)
end
# Is current user own current editing product?
def user_can_edit_product
@product = current_user.products.find_by(id: params[:id])
redirect_to root_url, flash: { alert: 'You do not have permission to edit this product' } if @product.blank?
end
end
\ No newline at end of file
module ProductsHelper
def get_product_thumbnail(product, thumbnail_width, thumbnail_height)
product.image_url ||
"product/placeholder_#{thumbnail_width}x#{thumbnail_height}"
default_img_path = "product/placeholder_#{thumbnail_width}x#{thumbnail_height}"
# product.image_url always returns PictureUploader object
product.image_url? ? product.image_url : default_img_path
end
end
\ No newline at end of file
class Product < ApplicationRecord
belongs_to :category
end
\ No newline at end of file
belongs_to :user
mount_uploader :image_url, PictureUploader
validates :category_id, presence: true
validates :user_id, presence: true
end
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :products, dependent: :destroy
end
class PictureUploader < CarrierWave::Uploader::Base
# Include RMagick or MiniMagick support:
# include CarrierWave::RMagick
# include CarrierWave::MiniMagick
# Choose what kind of storage to use for this uploader:
storage :file
# storage :fog
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
# Provide a default URL as a default if there hasn't been a file uploaded:
# def default_url(*args)
# # For Rails 3.1+ asset pipeline compatibility:
# # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
#
# "/images/fallback/" + [version_name, "default.png"].compact.join('_')
# end
# Process files as they are uploaded:
# process scale: [200, 300]
#
# def scale(width, height)
# # do something
# end
# Create different versions of your uploaded files:
# version :thumb do
# process resize_to_fit: [50, 50]
# end
# Add a white list of extensions which are allowed to be uploaded.
# For images you might use something like this:
# def extension_whitelist
# %w(jpg jpeg gif png)
# end
# Override the filename of the uploaded files:
# Avoid using model.id or version_name here, see uploader/store.rb for details.
# def filename
# "something.jpg" if original_filename
# end
end
<div class="container">
<div class="row">
<div class="col-md-9 col-md-push-3 my-account form-section">
<h1 class="h2 heading-primary font-weight-normal">
Edit <%= resource_name.to_s.humanize %>
</h1>
<div class="featured-box featured-box-primary featured-box-flat featured-box-text-left mt-md">
<div class="box-content">
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<h4 class="heading-primary text-uppercase mb-lg">
ACCOUNT INFORMATION
</h4>
<div class="row">
<div class="col-xs-12">
<div class="form-group">
<%= f.label :email %>
<%= f.email_field :email, autofocus: true, class: "form-control" %>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<%= f.label :password %>
<% if @minimum_password_length %>
<em>(<%= @minimum_password_length %> characters minimum)</em>
<% end %>
<%= f.password_field :password, autocomplete: "off", class: "form-control" %>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation, autocomplete: "off", class: "form-control" %>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
<%= f.password_field :current_password, autocomplete: "off", class: "form-control" %>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="form-action clearfix mt-none">
<%= f.submit "Submit", class: "btn btn-primary" %>
</div>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>
<section class="form-section register-form">
<div class="container">
<h1 class="h2 heading-primary font-weight-normal mb-md mt-xlg">
Create an Account
</h1>
<div class="featured-box featured-box-primary featured-box-flat featured-box-text-left mt-md">
<div class="box-content">
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<h4 class="heading-primary text-uppercase mb-lg">PERSONAL INFORMATION</h4>
<%= devise_error_messages! %>
<div class="row">
<div class="col-xs-12">
<div class="form-group">
<%= f.label :email %>
<%= f.email_field :email, autofocus: true, class: "form-control" %>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<%= f.label :password %>
<% if @minimum_password_length %>
<em>(<%= @minimum_password_length %> characters minimum)</em>
<% end %>
<%= f.password_field :password, autocomplete: "off", class: "form-control" %>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation, autocomplete: "off", class: "form-control" %>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="form-action clearfix mt-none">
<%= f.submit "Submit", class: "btn btn-primary" %>
</div>
</div>
</div>
<% end %>
</div>
</div>
</div>
</section>
<section class="form-section">
<div class="container">
<h2 class="h2 heading-primary font-weight-normal mb-md mt-xlg">
Welcome back! Sign in to your account
</h2>
<div class="featured-box featured-box-primary featured-box-flat featured-box-text-left mt-md">
<div class="box-content">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<div class="form-content">
<h3 class="heading-text-color font-weight-normal">
</h3>
<div class="form-group">
<%= f.label :email %>
<%= f.email_field :email, autofocus: true, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password %>
<%= f.password_field :password, autocomplete: "off", class: "form-control" %>
</div>
</div>
<div class="form-action clearfix">
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<%= link_to "Lost your password?", new_password_path(resource_name), class: 'pull-left' %>
<% end -%>
<%= f.submit "Submit", class: "btn btn-primary" %>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>
</section>
\ No newline at end of file
<%- if controller_name != 'sessions' %>
<%= link_to "Log in", new_session_path(resource_name) %><br />
<% end -%>
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
<%= link_to "Sign up", new_registration_path(resource_name) %><br />
<% end -%>
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
<%= link_to "Forgot your password?", new_password_path(resource_name) %><br />
<% end -%>
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %><br />
<% end -%>
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %><br />
<% end -%>
<%- if devise_mapping.omniauthable? %>
<%- resource_class.omniauth_providers.each do |provider| %>
<%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %><br />
<% end -%>
<% end -%>
<footer id="footer">
<div class="footer-copyright">
<div class="container">
<a href="" class="logo">
<%= image_tag('logo-footer.png', class: 'img-responsive') %>
</a>
<p class="copyright-text">
© Copyright 2017. All Rights Reserved.
</p>
</div>
</div>
</footer>
\ No newline at end of file
<header id="header">
<div class="header-body">
<div class="header-top">
<div class="container">
</div>
</div>
<div class="header-container container">
<div class="header-row">
<div class="header-column">
<div class="header-logo">
<a href="<%= root_url %>">
<%= image_tag 'logo-header.png' %>
</a>
</div>
</div>
<div class="header-column">
<div class="header-row">
<div class="cart-area">
</div>
<div class="header-search">
</div>
</div>
</div>
</div>
</div>
<div class="header-container header-nav">
</div>
</div>
</header>
\ No newline at end of file
......@@ -9,12 +9,10 @@
</head>
<body>
<div id="main-container col2-left-layout">
<div class="container">
<%= yield %>
</div>
<!-- End .container -->
<%= render 'layouts/header' %>
<div role="main" class="main">
<%= yield %>
</div>
<!-- End .main-container -->
<%= render 'layouts/footer' %>
</body>
</html>
<%= form_for(@product) do |f| %>
<%= render 'shared/error_messages', object: @product %>
<div class="row">
<div class="col-xs-12">
<div class="form-group">
<%= f.label :title %>
<%= f.text_field :title, class: "form-control" %>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<%= f.label :sku %>
<%= f.text_field :sku, class: "form-control", step: :any %>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<%= f.label :price %>
<%= f.number_field :price, class: "form-control" %>
</div>
</div>
<div class="col-xs-12">
<div class="form-group">
<%= f.label :description %>
<%= f.text_area :description, class: "form-control", rows: 7 %>
</div>
</div>
<div class="col-xs-12">
<div class="form-group">
<%= f.label :category, 'Product Category' %>
<%= f.collection_select :category_id, Category.all, :id, :title, { prompt: 'Select a category' }, class: 'form-control' %>
</div>
</div>
<div class="col-xs-12">
<div class="form-group">
<%= f.label :image_url, 'Product Image' %>
<%= f.file_field :image_url %>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="form-action clearfix mt-none">
<%= f.submit "Submit", class: "btn btn-primary" %>
</div>
</div>
</div>
</div>
<% end %>
\ No newline at end of file
<li class="item">
<div class="product-img">
<a href="#">
<figure>
<%= image_tag(get_product_thumbnail(product, 170, 204), class: "small-image") %>
</figure>
</a>
</div>
<div class="product-shop">
<h2 class="product-name"><%= product.title %></h2>
<div class="price-box">
<div class="special-price">
<span class="price"><%= number_to_currency(product.price) %></span>
<li class="product-<%= product.id %>">
<div class="product product-list">
<figure class="product-image-area">
<%= link_to image_tag(get_product_thumbnail(product, 170, 204)), product_path(product) %>
</figure>
<div class="product-details-area">
<h2 class="product-name">
<%= link_to product.title, product_url(product) %>
</h2>
<div class="product-short-desc">
<%= product.description %>
</div>
<div class="product-price-box">
<span class="product-price"><%= number_to_currency(product.price) %></span>
</div>
</div>
<div class="desc std">
<%= simple_format(product.description) %>
</div>
</div>
</li>
\ No newline at end of file
</li>
<ul class="products-grid">
<% @recommended_products.each do |product| %>
<li class="item col-lg-4 col-md-4 col-sm-6 col-xs-6 ">
<div class="product-item">
<div class="item-inner">
<div class="product-thumb">
<figure>
<a href="#">
<%= image_tag(get_product_thumbnail(product, 170, 204)) %>
</a>
<li class="product-<%= product.id %>">
<div class="product">
<figure class="product-image-area">
<%= link_to image_tag(get_product_thumbnail(product, 170, 204)), product_path(product) %>
</figure>
</div>
<div class="item-info">
<div class="info-inner">
<div class="item-title">
<%= link_to product.title, "#" %>
</div>
<div class="item-content">
<div class="item-price">
<div class="price-box">
<div class="regular-price">
<span class="price"><%= number_to_currency(product.price) %></span>
</div>
</div>
</div>
<div class="product-details-area">
<h2 class="product-name">
<%= link_to product.title, product_url(product) %>
</h2>
<div class="product-price-box">
<span class="product-price"><%= number_to_currency(product.price) %></span>
</div>
</div>
</div>
</div>
</div>
</li>
<% end %>
</ul>
\ No newline at end of file
</li>
<% end %>
\ No newline at end of file
<div class="container">
<div class="row">
<div class="col-md-9 col-md-push-3 create-product form-section">
<h1 class="h2 heading-primary font-weight-normal">
Edit Product #<%= @product.id %>
(<%= link_to 'Delete', @product, method: :delete, data: { confirm: 'Are your sure?' } %>)
</h1>
<div class="featured-box featured-box-primary featured-box-flat featured-box-text-left mt-md">
<div class="box-content">
<%= render 'form' %>
</div>
</div>
</div>
</div>
</div>
\ No newline at end of file
<div class="container">
<div class="row">
<div class="col-md-9 col-md-push-3 create-product form-section">
<h1 class="h2 heading-primary font-weight-normal">
New Product
</h1>
<div class="featured-box featured-box-primary featured-box-flat featured-box-text-left mt-md">
<div class="box-content">
<%= render 'form' %>
</div>
</div>
</div>
</div>
</div>
\ No newline at end of file
<div class="container">
<div class="product-view">
<div class="product-essential">
<div class="row">
<div class="product-img-box col-sm-5">
<div class="product-img-box-wrapper">
<div class="product-img-wrapper">
<%= image_tag(get_product_thumbnail(@product, 715, 952)) %>
</div>
</div>
</div>
<div class="product-details-box col-sm-7">
<h1 class="product-name">
<%= @product.title %>
</h1>
<div class="product-short-desc">
<%= @product.description %>
</div>
<div class="product-detail-info">
<div class="product-price-box">
<span class="product-price"><%= number_to_currency(@product.price) %></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
\ No newline at end of file
<% if object.errors.any? %>
<div id="error_explanation" class="alert alert-danger">
<h4><%= pluralize(object.errors.count, "error") %> prohibited your data from being saved:</h4>
<ul>
<% object.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
\ No newline at end of file
<% flash.each do |message_type, message| %>
<div class="alert alert-<%= message_type %>">
<span><%= message %></span>
</div>
<% end %>
\ No newline at end of file
<div class="panel-group">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
Categories
</h4>
</div>
<div id="panel-filter-category">
<div class="panel-body">
<ul>
</ul>
</div>
</div>
</div>
</div>
\ No newline at end of file
<div class="row">
<div class="col-main col-sm-9 col-xs-12 col-sm-push-3">
<div class="shop-inner">
<div class="page-title">
<h1>Recommended Items</h1>
</div>
<div class="product-grid-area">
<div class="container">
<div class="row">
<div class="col-md-9 col-md-push-3">
<%= render 'shared/flash_messages' %>
<h2 class="h2 heading-primary mt-lg clearfix">
<span>Recommended Items</span>
</h2>
<div class="products-grid columns3">
<%= render 'products/recommended' %>
</div>
<div class="page-title">
<h1>Newest Items</h1>
</div>
<div class="product-list-area">
<ul id="products-list" class="products-list">
<%= render @latest_products %>
</ul>
</div>
<h2 class="h2 heading-primary mt-lg clearfix">
<span>Newest Items</span>
</h2>
<ul class="products-list">
<%= render @latest_products %>
</ul>
<div class="pagination-area">
<%= paginate @latest_products %>
<div class="toolbar-bottom">
<div class="toolbar">
<div class="sorter">
<%= paginate @latest_products, theme: 'bootstrap' %>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-md-pull-9 sidebar shop-sidebar">
<%= render 'shared/sidebar' %>
</div>
</div>
<aside class="sidebar col-sm-3 col-xs-12 col-sm-pull-9">
<%= render 'shared/sidebar' %>
</aside>
</div>
# MySQL. Versions 5.1.10 and up are supported.
#
# Install the MySQL driver
# gem install mysql2
#
# Ensure the MySQL gem is defined in your Gemfile
# gem 'mysql2'
#
# And be sure to use new-style password hashing:
# http://dev.mysql.com/doc/refman/5.7/en/old-client.html
#
default: &default
adapter: mysql2
encoding: utf8
username: root
password: 123123
development:
<<: *default
database: dhp_venshop_development
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: dhp_venshop_test
# As with config/secrets.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
# Instead, provide the password as a unix environment variable when you boot
# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
# for a full rundown on how to provide these environment variables in a
# production deployment.
#
# On Heroku and other platform providers, you may have a full connection URL
# available as an environment variable. For example:
#
# DATABASE_URL="mysql2://myuser:mypass@localhost/somedatabase"
#
# You can use this database configuration with:
#
# production:
# url: <%= ENV['DATABASE_URL'] %>
#
production:
<<: *default
database: dhp_venshop_production
username: dhp_venshop
password: <%= ENV['DHP_VENSHOP_DATABASE_PASSWORD'] %>
# Additional translations at https://github.com/plataformatec/devise/wiki/I18n
en:
devise:
confirmations:
confirmed: "Your email address has been successfully confirmed."
send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
failure:
already_authenticated: "You are already signed in."
inactive: "Your account is not activated yet."
invalid: "Invalid %{authentication_keys} or password."
locked: "Your account is locked."
last_attempt: "You have one more attempt before your account is locked."
not_found_in_database: "Invalid %{authentication_keys} or password."
timeout: "Your session expired. Please sign in again to continue."
unauthenticated: "You need to sign in or sign up before continuing."
unconfirmed: "You have to confirm your email address before continuing."
mailer:
confirmation_instructions:
subject: "Confirmation instructions"
reset_password_instructions:
subject: "Reset password instructions"
unlock_instructions:
subject: "Unlock instructions"
email_changed:
subject: "Email Changed"
password_change:
subject: "Password Changed"
omniauth_callbacks:
failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
success: "Successfully authenticated from %{kind} account."
passwords:
no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
updated: "Your password has been changed successfully. You are now signed in."
updated_not_active: "Your password has been changed successfully."
registrations:
destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
signed_up: "Welcome! You have signed up successfully."
signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address."
updated: "Your account has been updated successfully."
sessions:
signed_in: "Signed in successfully."
signed_out: "Signed out successfully."
already_signed_out: "Signed out successfully."
unlocks:
send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes."
send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes."
unlocked: "Your account has been unlocked successfully. Please sign in to continue."
errors:
messages:
already_confirmed: "was already confirmed, please try signing in"
confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
expired: "has expired, please request a new one"
not_found: "not found"
not_locked: "was not locked"
not_saved:
one: "1 error prohibited this %{resource} from being saved:"
other: "%{count} errors prohibited this %{resource} from being saved:"
......@@ -2,4 +2,6 @@ Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
root 'static_pages#index'
resources :categories
resources :products
devise_for :users
end
......@@ -3,7 +3,7 @@ class CreateProducts < ActiveRecord::Migration[5.1]
create_table :products do |t|
t.string :title
t.text :description
t.text :sku
t.string :sku
t.decimal :price, precision: 8, scale: 2
t.references :category, index: true
t.timestamps
......
class DeviseCreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
## Rememberable
t.datetime :remember_created_at
## Trackable
t.integer :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string :current_sign_in_ip
t.string :last_sign_in_ip
## Confirmable
# t.string :confirmation_token
# t.datetime :confirmed_at
# t.datetime :confirmation_sent_at
# t.string :unconfirmed_email # Only if using reconfirmable
## Lockable
# t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
# t.string :unlock_token # Only if unlock strategy is :email or :both
# t.datetime :locked_at
t.timestamps null: false
end
add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
# add_index :users, :confirmation_token, unique: true
# add_index :users, :unlock_token, unique: true
end
end
class AddUserRefToProducts < ActiveRecord::Migration[5.1]
def change
add_reference :products, :user, index: true
end
end
......@@ -10,24 +10,63 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170606064132) do
ActiveRecord::Schema.define(version: 20170608073720) do
create_table "categories", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.string "title"
t.text "description"
end
create_table "line_items", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.integer "quantity", default: 1
t.bigint "product_id"
t.bigint "order_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["order_id"], name: "index_line_items_on_order_id"
t.index ["product_id"], name: "index_line_items_on_product_id"
end
create_table "orders", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.bigint "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_orders_on_user_id"
end
create_table "products", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.string "title"
t.text "description"
t.text "sku"
t.string "sku"
t.decimal "price", precision: 8, scale: 2
t.bigint "category_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "image_url"
t.bigint "user_id"
t.index ["category_id"], name: "index_products_on_category_id"
t.index ["user_id"], name: "index_products_on_user_id"
end
create_table "users", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "username"
t.string "first_name"
t.string "last_name"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
add_foreign_key "products", "categories"
add_foreign_key "products", "users"
end
......@@ -6,27 +6,39 @@
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
# Character.create(name: 'Luke', movie: movies.first)
# Delete all categories
# Delete all existed users
User.destroy_all
# Create sample users
4.times do
User.create!(email: Faker::Internet.safe_email,
password: 'password',
password_confirmation: 'password')
end
# Delete all existed categories
Category.destroy_all
# Create Product Categories
4.times do |n|
title = "Category #{n}"
desc = Faker::Lorem.paragraphs
Category.create!(title: title, description: desc)
Category.create!(title: "Category #{n + 1}",
description: Faker::Lorem.paragraphs)
end
# Create Products
categories = Category.all
80.times do
categories.each do |cat|
title = Faker::Commerce.product_name
desc = Faker::Lorem.paragraphs
sku = "u-#{rand(1..999)}"
price = Faker::Commerce.price
cat.products.create!(title: title,
description: desc,
sku: sku,
price: price)
cat.products.create!(title: Faker::Commerce.product_name,
description: Faker::Lorem.paragraphs,
sku: "u-#{rand(1..999)}",
price: Faker::Commerce.price,
user_id: User.pluck(:id).shuffle[1])
end
end
\ No newline at end of file
end
# Create super user
User.create!(email: 'admin@example.com',
password: 'password',
password_confirmation: 'password',
super_admin: true)
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment