Merge pull request #72 from ZeusWPI/barcode

Barcode
This commit is contained in:
benji 2015-10-07 14:55:11 +02:00
commit 88096fc46a
73 changed files with 752 additions and 431 deletions

BIN
app/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -15,3 +15,12 @@
//= require bootstrap
//= require turbolinks
//= require_tree .
parseIntNaN = function(value) {
parsed_value = parseInt(value, 10);
if (isNaN(parsed_value)) {
return 0;
} else {
return parsed_value;
}
}

View file

@ -0,0 +1,12 @@
ready = function() {
increment_function = function() {
target = $(this).data("target");
$(target).val(parseIntNaN($(target).data("default")) + parseIntNaN($(this).val()));
}
$('[data-increment]').change(increment_function);
$('[data-increment]').keyup(increment_function);
}
$(document).ready(ready);
$(document).on('page:load', ready);

View file

@ -2,64 +2,75 @@
// All this logic will automatically be available in application.js.
// You can use CoffeeScript in this file: http://coffeescript.org/
ready = function() {
$('.btn-inc').on('click', function() {
increment($(this), 1);
/* INITIALIZE */
$('tr.order_item_wrapper').hide();
$('tr.order_item_wrapper').filter(function() {
return parseIntNaN($(this).find('[type=number]').val()) > 0;
}).show();
/* HELPERS */
increment_product = function(product_id) {
input = $("#current_order").find(".order_item_wrapper[data-product=" + product_id + "]").find("input[type=number]");
$(input).val(parseIntNaN($(input).val()) + 1).change();
}
recalculate = function() {
/* Total Price */
array = $('tr.order_item_wrapper').map(function() {
return parseIntNaN($(this).data("price")) * parseIntNaN($(this).find("input[type=number]").val());
})
sum = 0;
array.each(function(i, el) { sum += el; });
$("#current_order .total_price").html((sum / 100.0).toFixed(2));
/* Message when no product has been choosen */
$("#current_order #empty").toggle(!($('tr.order_item_wrapper input[type=number]').filter(function() {
return parseIntNaN($(this).val()) > 0;
}).length));
}
/* PRODUCT MODAL */
products_ordered = $('#product_search').keyup(function () {
var rex = new RegExp($(this).val(), 'i');
$('[data-name]').hide();
$('[data-name]').filter(function () {
return rex.test($(this).data("name"));
}).show();
})
$('#products_modal').on('hidden.bs.modal', function () {
$('#product_search').val('');
});
$('.btn-dec').on('click', function() {
increment($(this), -1);
});
$('.form_row').each(function(index, row) {
updateInput(row, false);
$(row).on('input', function() {
updateInput(row);
$("#products_modal button").click(function() {
increment_product($(this).data("product"))
})
/* BARCODE SCAN */
$("#from_barcode_form").submit(function(event) {
event.preventDefault();
barcode = $(this).find("input[type=number]").val();
$.ajax({
url: "/barcodes/" + barcode,
success: function(data) {
increment_product(data["id"]);
$("#from_barcode_form")[0].reset();
},
dataMethod: "json"
}).fail(function() {
alert("Barcode '" + barcode + "' was not found in the database system.");
});
});
/* CURRENT ORDER CHANGE */
$('tr.order_item_wrapper input[type=number]').change(function() {
tr = $(this).closest('tr.order_item_wrapper')
$(tr).toggle(parseIntNaN($(this).val()) > 0);
$(tr).find("td").last().html(((parseIntNaN($(tr).data("price")) * parseIntNaN($(this).val())) / 100.0).toFixed(2))
recalculate();
})
recalculate();
};
// Validate input, and then update
updateInput = function(row, useRecalculate) {
if (useRecalculate == null) {
useRecalculate = true;
}
cell = row.querySelector("input");
if (!cell.validity.valid) {
if (parseInt(cell.value) > parseInt(cell.max)) {
cell.value = parseInt(cell.max);
} else {
cell.value = 0;
}
}
disIfNec(row)
if (useRecalculate) {
recalculate()
}
};
disIfNec = function(row) {
counter = parseInt($(row).find('.row_counter').val())
$(row).find('.btn-dec').prop('disabled', counter === 0)
$(row).find('.btn-inc').prop('disabled', counter === parseInt($(row).find('.row_counter').attr('max')))
};
recalculate = function() {
sum = 0
$('.row_counter').each(function(i, value) { sum += parseInt($(value).val()) * parseInt($(value).data('price')) })
return $('#order_price').html((sum / 100.0).toFixed(2))
};
increment = function(button, n) {
row = $(button).closest('.form_row')
// Fix the counter
counter = $(row).find('.row_counter')
value = parseInt(counter.val())
if (isNaN(value)) {
value = 0
}
counter.val(value + n)
updateInput(row[0])
}
$(document).ready(ready);

View file

@ -1,3 +0,0 @@
// Place all the styles related to the admins controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -1,3 +0,0 @@
// Place all the styles related to the callbacks controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -2,60 +2,81 @@
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
.big-form-button {
height: 65px;
width: 65px;
}
.form-control.big-form-field {
height: 65px;
text-align: center;
}
.form_row_image {
height: 100px;
width: 100px;
margin-left: auto;
margin-right: auto;
position: relative;
img {
position: absolute;
top: 0;
bottom: 0;
left: 0;
top: 0;
margin: auto;
.barcode-wrapper {
font-size: 300%;
input {
width: 100%;
}
}
.form_row input::-webkit-outer-spin-button,
.form_row input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
#products_modal {
.modal-header {
h4 {
display: inline-block;
}
input {
margin-right: 20px;
}
}
}
.form_row .btn-lg {
padding: 10px 10px;
}
.form_row .caption {
h4 {
position: relative;
span {
#product_buttons {
.col-md-2, .col-md-3 {
margin-bottom: 20px;
}
button.product {
width: 100%;
p {
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
display: inline-block;
padding-right: 50px;
}
small {
margin-left: -45px;
position: absolute;
top: 5px;
img {
max-width: 100%;
margin-left: auto;
margin-right: auto;
display: block;
}
}
}
#order_price {
width: 50px;
#current_order {
text-align: left;
border: 1px solid;
padding: 10px;
div.center {
margin-bottom: 20px;
}
.order_item_wrapper {
input {
border: none;
width: auto;
}
}
table {
border-top: 1px dashed;
border-collapse: separate;
margin-bottom: 15px;
td:first-child {
width: 40px;
input {
text-align: right;
width: 40px;
}
}
tr:last-child td {
border-top: 1px dashed;
padding-top: 10px;
margin-top: 10px;
}
tr.margin {
height: 10px;
}
td.euro {
text-align: right;
&::before {
content: "";
}
}
}
}

View file

@ -1,3 +0,0 @@
// Place all the styles related to the sessions controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -1,3 +0,0 @@
// Place all the styles related to the stock controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -0,0 +1,22 @@
#stock_entry {
border: 1px solid #ccc;
background-color: #F5F5F5;
padding: 20px;
border-radius: 8px;
-moz-border-radius: 8px;
-webkit-border-radius: 8px;
table {
margin-bottom: 20px;
border-spacing: 10px;
border-collapse: separate;
tr:last-child td {
border-top: 1px dashed;
padding-top: 10px;
}
td {
/* padding: 10px; */
}
}
}

View file

@ -4,12 +4,12 @@
//signin
.sign-in{
.checkbox label{
padding-left: 0px;
.sign-in {
.checkbox label {
padding-left: 0px;
}
#user_remember_me{
margin-left: 10px;
#user_remember_me {
margin-left: 10px;
}
}
@ -17,7 +17,7 @@
padding: 0px;
min-height: 280px;
border: 3px solid #333;
.header{
.header {
border-bottom: 3px solid #333;
text-align: center;
color: #fff;
@ -26,19 +26,19 @@
margin: 0px;
}
.caption{
.avatar{
.caption {
.avatar {
float: right;
height: 70px;
width: 70px;
}
}
.footer{
.footer {
width:80%;
border-top: 1px dashed #333;
margin:10%;
margin-bottom: 5px;
.btn{
.btn {
width:100%;
margin-top:10px;
margin-bottom:0px;

View file

@ -1,3 +0,0 @@
// Place all the styles related to the welcome controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -1,6 +1,5 @@
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
check_authorization
rescue_from CanCan::AccessDenied do |exception|
redirect_to root_path, flash: { error: exception.message }

View file

@ -0,0 +1,18 @@
class BarcodesController < ApplicationController
load_and_authorize_resource :barcode, shallow: true
def create
@barcode.save
redirect_to barcode_products_path, notice: "Barcode successfully linked!"
end
def show
render json: @barcode.product
end
private
def barcode_params
params.require(:barcode).permit(:code)
end
end

View file

@ -1,6 +1,4 @@
class CallbacksController < Devise::OmniauthCallbacksController
skip_authorization_check
def zeuswpi
@user = User.from_omniauth(request.env["omniauth.auth"])
sign_in_and_redirect @user

View file

@ -3,10 +3,8 @@ class OrdersController < ApplicationController
load_and_authorize_resource :order, through: :user, shallow: true
def new
products = (@user.products.for_sale.select("products.*", "sum(order_items.count) as count").group(:product_id).order("count desc") | Product.for_sale)
products.each do |p|
@order.order_items.build(product: p)
end
@products = Product.all.for_sale.order(:name)
@order.products << @products
end
def create
@ -14,6 +12,7 @@ class OrdersController < ApplicationController
flash[:success] = "#{@order.to_sentence} ordered. Enjoy it!"
redirect_to root_path
else
@products = Product.all.for_sale.order(:name)
render 'new'
end
end

View file

@ -3,15 +3,26 @@ class ProductsController < ApplicationController
respond_to :html, :js
def new
end
def create
if @product.save
flash[:success] = "Product created!"
redirect_to products_path
redirect_to barcode_products_path
else
render 'new'
render 'link'
end
end
def barcode
end
def load_barcode
@product = Barcode.find_by(code: params[:barcode]).try(:product)
if @product
render 'products/stock_entry'
else
@product = Product.new
@product.barcodes.build(code: params[:barcode])
render 'products/link'
end
end
@ -28,12 +39,15 @@ class ProductsController < ApplicationController
def update
@product.update_attributes product_params
respond_with @product
respond_to do |format|
format.js { respond_with @product }
format.html { redirect_to barcode_products_path, notice: "Stock has been updated!" }
end
end
private
def product_params
params.require(:product).permit(:name, :price, :avatar, :category, :stock, :calories, :deleted)
params.require(:product).permit(:name, :price, :avatar, :category, :stock, :calories, :deleted, :barcode, barcodes_attributes: [:code])
end
end

View file

@ -1,3 +0,0 @@
class SessionsController < Devise::SessionsController
skip_authorization_check
end

View file

@ -1,19 +0,0 @@
class StocksController < ApplicationController
load_and_authorize_resource
def new
Product.all.each do |p|
@stock.stock_entries << Stock::StockEntry.new(product: p)
end
end
def create
@stock = Stock.new(params[:stock])
if @stock.update
flash[:success] = "Stock updated!"
redirect_to products_path
else
render 'new'
end
end
end

View file

@ -18,16 +18,6 @@ class UsersController < ApplicationController
end
end
def index
@users = User.members
end
def destroy
@user.destroy
flash[:success] = "Succesfully removed user"
redirect_to users_path
end
def edit_dagschotel
@dagschotel = @user.dagschotel
@ -35,14 +25,6 @@ class UsersController < ApplicationController
@categories = Product.categories
end
def update_dagschotel
@user.dagschotel = Product.find(params[:product_id])
@user.save
flash[:success] = "Succesfully updated dagschotel"
redirect_to @user
end
def quickpay
order = @user.orders.build
order.order_items.build(count: 1, product: @user.dagschotel)
@ -57,7 +39,7 @@ class UsersController < ApplicationController
private
def user_params
params.require(:user).permit(:avatar, :private)
params.require(:user).permit(:avatar, :private, :dagschotel_id)
end
def init

View file

@ -1,6 +1,12 @@
class WelcomeController < ApplicationController
skip_authorization_check
skip_before_filter :verify_authenticity_token, only: :token_sign_in
def index
end
def token_sign_in
return head(:unauthorized) unless params[:token] == Rails.application.secrets.koelkast_token
koelkast = User.find_by(name: "koelkast")
sign_in_and_redirect koelkast
end
end

View file

@ -37,7 +37,12 @@ class FormattedFormBuilder < ActionView::Helpers::FormBuilder
options[:value] = number_with_precision(options[:value], precision: 2)
form_group_builder(name, options) do
number_field_without_format(name, options)
content_tag :div, class: "input-group" do
content_tag(:span, class: "input-group-addon") do
content_tag :span, nil, class: "glyphicon glyphicon-euro"
end +
number_field_without_format(name, options)
end
end
end

View file

@ -4,10 +4,14 @@ class Ability
def initialize(user)
return unless user
can :from_barcode, Product
if user.admin?
can :manage, :all
elsif user.koelkast?
can :manage, Order
can :manage, Order do |order|
!order.try(:user).try(:private)
end
can :quickpay, User
else
can :read, :all

20
app/models/barcode.rb Normal file
View file

@ -0,0 +1,20 @@
# == Schema Information
#
# Table name: barcodes
#
# id :integer not null, primary key
# product_id :integer
# code :string default(""), not null
# created_at :datetime
# updated_at :datetime
#
class Barcode < ActiveRecord::Base
include FriendlyId
friendly_id :code, use: :finders
belongs_to :product
# validates :product, presence: true
validates :code, presence: true, uniqueness: true
end

View file

@ -36,7 +36,7 @@ class Order < ActiveRecord::Base
private
def calculate_price
self.price_cents = self.order_items.map{ |oi| oi.count * oi.product.price_cents }.sum
self.price_cents = self.order_items.map{ |oi| oi.count * (oi.product.try(:price_cents) || 0) }.sum
end
def create_api_job

View file

@ -21,10 +21,12 @@ class Product < ActiveRecord::Base
include Avatarable
has_many :order_items
has_many :barcodes
accepts_nested_attributes_for :barcodes
enum category: %w(food beverages other)
validates :name, presence: true
validates :name, presence: true, uniqueness: true
validates :price_cents, presence: true, numericality: { only_integer: true, greater_than: 0 }
validates :stock, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :calories, numericality: { only_integer: true, allow_nil: true, greater_than_or_equal_to: 0 }
@ -39,8 +41,4 @@ class Product < ActiveRecord::Base
if value.is_a? String then value.sub!(',', '.') end
self.price_cents = (value.to_f * 100).to_int
end
def take_out_of_sale!
update_attribute :deleted, true
end
end

View file

@ -6,11 +6,6 @@
# created_at :datetime
# updated_at :datetime
# remember_created_at :datetime
# sign_in_count :integer default("0"), not null
# current_sign_in_at :datetime
# last_sign_in_at :datetime
# current_sign_in_ip :string
# last_sign_in_ip :string
# admin :boolean
# dagschotel_id :integer
# avatar_file_name :string
@ -28,12 +23,14 @@ class User < ActiveRecord::Base
include Statistics, Avatarable, FriendlyId
friendly_id :name, use: :finders
devise :database_authenticatable, :omniauthable, :omniauth_providers => [:zeuswpi]
devise :omniauthable, :omniauth_providers => [:zeuswpi]
has_many :orders, -> { includes :products }
has_many :products, through: :orders
belongs_to :dagschotel, class_name: 'Product'
validates :dagschotel, presence: true, if: -> { dagschotel_id }
scope :members, -> { where koelkast: false }
scope :publik, -> { where private: false }

View file

@ -1,26 +1,26 @@
#flash
- if flash[:error]
.alert.alert-danger.alert-dismissable
%button.close{"aria-hidden" => "true", "data-dismiss" => "alert", :type => "button"} ×
%button.close{"aria-hidden" => "true", "data-dismiss" => "alert", type: "button"} ×
%strong Error!
= flash[:error]
- if flash[:success]
.alert.alert-success.alert-dismissable
%button.close{"aria-hidden" => "true", "data-dismiss" => "alert", :type => "button"} ×
%button.close{"aria-hidden" => "true", "data-dismiss" => "alert", type: "button"} ×
%strong Success!
= raw flash[:success]
- if flash[:notice]
.alert.alert-info.alert-dismissable
%button.close{"aria-hidden" => "true", "data-dismiss" => "alert", :type => "button"} ×
%button.close{"aria-hidden" => "true", "data-dismiss" => "alert", type: "button"} ×
%strong Notice!
= flash[:notice]
- if flash[:warning]
.alert.alert-warning.alert-dismissable
%button.close{"aria-hidden" => "true", "data-dismiss" => "alert", :type => "button"} ×
%button.close{"aria-hidden" => "true", "data-dismiss" => "alert", type: "button"} ×
%strong Warning!
= flash[:warning]
- if flash[:alert]
.alert.alert-danger.alert-dismissable
%button.close{"aria-hidden" => "true", "data-dismiss" => "alert", :type => "button"} ×
%button.close{"aria-hidden" => "true", "data-dismiss" => "alert", type: "button"} ×
%strong Error!
= flash[:alert]

View file

@ -1,10 +1,8 @@
%h2 Sign in
= render partial: 'flash'
= content_for :title, "Sign in"
.sign-in
= f_form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f|
= f_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
= f.text_field :name
= f.password_field :password
- if devise_mapping.rememberable?
= f.check_box :remember_me
= f.submit "Sign in"
= render "devise/shared/links"

View file

@ -1,16 +0,0 @@
- unless controller_name == 'sessions'
= link_to "Log in", new_session_path(resource_name)
%br/
- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations'
= link_to "Forgot your password?", new_password_path(resource_name)
%br/
- if devise_mapping.confirmable? && controller_name != 'confirmations'
= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name)
%br/
- 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/
- if devise_mapping.omniauthable?
- resource_class.omniauth_providers.each do |provider|
= link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider), class: "btn btn-large btn-primary"
%br/

View file

@ -2,7 +2,7 @@
.container-fluid
/ Brand and toggle get grouped for better mobile display
.navbar-header
%button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", type: "button"}
%button.navbar-toggle{data: { target: ".navbar-collapse", toggle: "collapse" } }
%span.icon-bar
%span.icon-bar
%span.icon-bar
@ -11,11 +11,7 @@
- unless current_user && current_user.koelkast?
.collapse.navbar-collapse
.hidden-xs.navbar-form.navbar-right
.form-group
- if user_signed_in?
= link_to "Logout", destroy_user_session_path, class: "btn btn-default form-control"
- else
= link_to "Login", omniauth_authorize_path("user", "zeuswpi"), class: "btn btn-success form-control"
= render 'layouts/session_button'
%ul.nav.navbar-nav.navbar-right
%li= mail_to "tab@zeus.ugent.be", "Send feedback"
- if user_signed_in?
@ -26,26 +22,15 @@
%span.caret
%ul.dropdown-menu{role: "menu"}
%li= link_to "List", products_path
%li= link_to "Add product" , new_product_path
%li= link_to "Add stock", new_stock_path
%li.dropdown
%a.dropdown-toggle{"aria-expanded" => "false", "data-toggle" => "dropdown", href: "#", role: "button"}
Users
%span.caret
%ul.dropdown-menu{role: "menu"}
%li= link_to "List" , users_path
%li= link_to "Add product" , barcode_products_path
%li.dropdown
%a.dropdown-toggle{"data-toggle" => "dropdown", href: "#"}
Logged in as #{current_user.name}
%b.caret
%ul.dropdown-menu
%li= link_to "Edit avatar", edit_user_path(current_user)
%li= link_to "Edit profile", edit_user_path(current_user)
%li
%p.navbar-text
Balance: #{euro_from_cents(current_user.balance)}
.visible-xs.navbar-form
.form-group
- if user_signed_in?
= button_to "Logout", destroy_user_session_path, class: "btn btn-default form-control", method: :delete
- else
= link_to "Login", omniauth_authorize_path("user", "zeuswpi"), class: "btn btn-success form-control"
= render 'layouts/session_button'

View file

@ -0,0 +1,5 @@
.form-group
- if user_signed_in?
= link_to "Logout", destroy_user_session_path, class: "btn btn-default form-control"
- else
= link_to "Login", omniauth_authorize_path("user", "zeuswpi"), class: "btn btn-success form-control"

View file

@ -9,6 +9,8 @@
%body
= render 'layouts/header'
.container
%h2= yield :title
= render partial: 'flash'
= yield
= render 'layouts/footer'
= debug(params) if Rails.env.development?

View file

@ -1,5 +1,5 @@
.col-md-3.form_products
%div{class: "thumbnail#{' out-of-stock' if product.stock.zero?}"}
%div.thumbnail{ class: ('out-of-stock' if product.stock.zero?) }
.form_row.center
.form_row_image
= image_tag product.avatar

View file

@ -1,7 +0,0 @@
.col-md-3.form_total
%strong Total price
.input-group
%span.input-group-addon €
= content_tag :span, "", id: "order_price", class: "input-group-addon"
%span.input-group-btn
= f.submit "Order!", class: "btn btn-primary big-form-button", skip_wrapper: true

View file

@ -0,0 +1,19 @@
#products_modal.modal{ tabindex: -1 }
.modal-dialog.modal-lg
.modal-content
.modal-header
%button.close{ data: { dismiss: :modal } }
%span &times;
%h4.modal-title Kies een product
.col-xs-3.pull-right
%input#product_search.form-control{ placeholder: "Search" }
.modal-body
#product_buttons.container-fluid
- @products.each do |product|
.col-md-2{ data: { name: product.name } }
%button.btn.btn-default.product{ data: { product: product.id, dismiss: :modal } }
%p= product.name
= image_tag product.avatar(:dagschotel), class: "center"
.modal-footer
%button.btn.btn-default{ data: { dismiss: :modal } }
Close

View file

@ -1,9 +1,42 @@
%h3
Order for #{@user.name} (Huidige schuld: #{euro_from_cents(@user.balance)})
.row
= f_form_for [@user, @order] do |f|
= f.error_messages
.col-md-12
= f.fields_for :order_items do |op_field|
= render op_field.object, f: op_field, product: op_field.object.product
= render 'orders/price', f: f
.col-md-6.col-md-offset-1.barcode-wrapper
.center
%h1 Order for #{@user.name}
= form_tag nil, id: "from_barcode_form" do
%input.center-block{ type: :number, name: :id, autofocus: true }
= "- OR -"
%button.btn.btn-default.center-block{ data: { toggle: :modal, target: "#products_modal" } }
Select Product Without Barcode
.col-md-4.col-md-offset-1
= form_for [@user, @order] do |f|
= render 'errors', object: @order
#current_order
.div.center
= image_tag "logo.png"
%table
%tr.margin
= f.fields_for :order_items do |ff|
%tr.order_item_wrapper{ data: { product: ff.object.product.id, price: ff.object.product.price_cents } }
%td
= ff.number_field :count
= ff.fields_for :product do |fff|
/ Needed for haml
%td
x
%td
%span= ff.object.product.name
%td.euro
= euro_from_cents(ff.object.product.price_cents * ff.object.count)
%tr#empty
%td
%td
%em Empty Order.
%tr.margin
%tr
%td
%td
%td.text-right
Total:
%td.total_price.euro
= f.submit "Order!", class: "btn btn-primary form-control"
= render 'products_modal'

View file

@ -1,6 +1,4 @@
= render partial: 'flash'
.warning.center
%h1 TESTFASE | GELIEVE STREEPJES TE BLIJVEN ZETTEN | TESTFASE
.row
- @users.each do |user|
= render 'users/new_order', user: user
= render @users

View file

@ -1,11 +1,11 @@
.row
.col-md-6.col-md-offset-3.sign-in
= f_form_for @product, html: { multipart: true } do |f|
= f.error_messages
= f.text_field :name
= f.price_field :price
= f.collection_select :category, Product.categories.keys
= f.number_field :stock
= f.number_field :calories
= f.file_field :avatar
= f.submit
= f_form_for @product, html: { multipart: true } do |f|
= f.error_messages
= f.text_field :name
= f.price_field :price
= f.collection_select :category, Product.categories.keys
= f.number_field :stock
= f.number_field :calories
= f.file_field :avatar
= f.fields_for :barcodes do |ff|
= ff.number_field :code, readonly: true
= f.submit

View file

@ -1,9 +1,9 @@
- if controller_name == 'products' && current_user && current_user.admin?
- if controller_name == 'products' && can?(:manage, Product)
= link_to "Edit", edit_product_path(product), class: "btn btn-default"
= link_to "Delete", product_path(product), method: :delete, class: "btn btn-danger", data: {confirm: 'Are you sure?'}
- if controller_name == 'users'
.product_dagschotel
- if current_user.dagschotel != product
= link_to "Make dagschotel", dagschotel_user_path(current_user, product), class: "btn btn-default"
= button_to "Make dagschotel", { controller: 'users', action: 'update', "user[dagschotel_id]" => product.id }, method: :put, class: "btn btn-default"
- else
= link_to "Huidige dagschotel", dagschotel_user_path(current_user, product), class: "btn btn-success", disabled: true
%span.btn.btn-success= "Current dagschotel"

View file

@ -1,6 +1,6 @@
.col-md-3
.thumbnail.pic
.form_row_image
.center
= image_tag product.avatar
.caption
= kcal_tag product.calories

View file

@ -0,0 +1,6 @@
.row
.col-md-6.col-md-offset-3.center
%h2 Scan barcode
= form_tag load_barcode_products_path do
.barcode-wrapper
%input.center-block{ type: :number, name: :barcode, autofocus: true }

View file

@ -1,2 +1,2 @@
%h1 Update product
= content_for :title, "Update product"
= render "form"

View file

@ -1,3 +1,2 @@
%h1 All products
= render partial: 'flash'
= content_for :title, "All products"
= render 'products/index'

View file

@ -0,0 +1,12 @@
.row
.col-md-7
%h4.pull-right Select a product to link the barcode to an existing product ...
#product_buttons.row
- Product.all.each do |product|
.col-md-3
= button_to product_barcodes_path(product), class: "btn btn-default product", data: { product: product.id, dismiss: :modal }, params: { "barcode[code]" => params[:barcode] } do
%p= product.name
= image_tag product.avatar(:dagschotel), class: "center"
.col-md-5
%h4 or create a new one
= render 'products/form'

View file

@ -1,2 +1,2 @@
%h1 New product
= content_for :title, "New product"
= render "form"

View file

@ -0,0 +1,15 @@
.row
#stock_entry.col-md-6.col-md-offset-3
= form_for @product do |f|
%table
%tr
%td Current stock
%td= @product.stock
%tr
%td Purchase
%td
%input.form-control.new_stock{ type: :number, autofocus: true, data: { increment: true, target: "#product_stock" }}
%tr
%td New stock
%td= f.number_field :stock, data:{ default: f.object.stock }, class: "form-control"
= f.submit "Update stock", class: "btn btn-primary form-control"

View file

@ -1,9 +1,9 @@
%tr{:id => "products_row_#{dom_id(product)}"}
%td= image_tag product.avatar(:small)
%tr{id: "products_row_#{dom_id(product)}"}
%td= link_to image_tag(product.avatar(:small)), edit_product_path(product)
%td= product.name
%td= euro(product.price)
%td= product.stock
%td
%span{:class => "glyphicon #{product.deleted ? "glyphicon-check" : "glyphicon-unchecked"}"}
%span{class: "glyphicon #{product.deleted ? "glyphicon-check" : "glyphicon-unchecked"}"}
%td= product.calories
%td= button_to "Edit", edit_product_path(product), method: :get, class: "btn btn-default", remote: true
%td= link_to "Edit", edit_product_path(product), class: "btn btn-default", remote: true

View file

@ -1,9 +1,8 @@
= content_for :title, "Products"
#products-errors
.row.products
.col-md-8.col-md-offset-2
%h1 Products
= render partial: 'flash'
= link_to "Add Stock", new_stock_path, class: "btn btn-default"
= link_to "Add products", barcode_products_path, class: "btn btn-default"
%table#products-table.table.table-striped
%tr
%th

View file

@ -1,8 +0,0 @@
- unless @stock.valid?
.panel.panel-danger.form-errors
.panel-heading
= "#{pluralize(@stock.errors.count + @stock.stock_entries.map(&:errors).map(&:count).sum, "error")} prohibited this stock from being saved:"
.panel-body
%ul
= @stock.errors.full_messages.map{ |m| content_tag(:li, m) }.join.html_safe
= @stock.stock_entries.map{ |se| se.errors.full_messages.map{ |e| "#{se.product.name}: #{e}" } }.flatten.map{ |m| content_tag(:li, m) }.join.html_safe

View file

@ -1,13 +0,0 @@
.row
.col-md-6.col-md-offset-3
%h2 Add stock
= f_form_for @stock do |f|
= render 'stocks/errors'
= f.fields_for :stock_entries do |se_field|
.row
.col-sm-3
= image_tag se_field.object.product.avatar
.col-sm-9
= se_field.hidden_field :product_id
= se_field.number_field :count, skip_label: true
= f.submit "Insert stock", class: 'btn btn-primary'

View file

@ -1,6 +0,0 @@
.col-md-2.overviewthumbnail
- unless user.dagschotel.nil?
= link_to quickpay_user_path(user) do
= image_tag user.dagschotel.avatar(:dagschotel), class: "img-circle dagschotel"
= link_to image_tag(user.avatar(:large) , class: "img-circle avatar"), new_user_order_path(user)
= link_to user.name , new_user_order_path(user), class: "btn btn-info", style: get_color_style(user)

View file

@ -1,8 +1,6 @@
%tr
%td= user.id
%td= image_tag user.avatar(:small)
%td= user.name
%td= euro(user.debt)
- if current_user.admin?
%td
= link_to "Delete", user_path(user), method: :delete, class: "btn btn-danger", data: { confirm: "Are you sure?" }
.col-md-2.overviewthumbnail
- unless user.dagschotel.nil?
= link_to quickpay_user_path(user) do
= image_tag user.dagschotel.avatar(:dagschotel), class: "img-circle dagschotel"
= link_to image_tag(user.avatar(:large) , class: "img-circle avatar"), new_user_order_path(user)
= link_to user.name , new_user_order_path(user), class: "btn btn-info", style: get_color_style(user)

View file

@ -1,4 +1,3 @@
= render 'flash'
.row
= render 'sidebar'
.col-sm-9

View file

@ -1,3 +1,2 @@
%h3
Choose new Dagschotel
= render 'products/index'
= content_for :title, "Choose new Dagschotel"
= render 'products/index'

View file

@ -1,12 +0,0 @@
.row.users
.col-md-8.col-md-offset-2
%h1 All users
= render partial: 'flash'
%table#users-table.table.table-striped
%tr
%th ID
%th
%th Nickname
%th Debt
%th
= render @users

View file

@ -1,4 +1,3 @@
= render partial: 'flash'
.row
= render 'sidebar'
#user_info.col-sm-9

View file

@ -1,7 +1,4 @@
%h2 Login
= render 'flash'
= content_for :title, "Login"
If this is the first time you log in, an account will be created for you.
%div
%br/
= link_to "Sign in with Zeus WPI account.", omniauth_authorize_path("user", "zeuswpi"), class: "btn btn-large btn-primary"
%br/

View file

@ -1,10 +1,9 @@
Rails.application.routes.draw do
devise_for :users, controllers: {
omniauth_callbacks: "callbacks",
sessions: "sessions"
}
devise_for :users, controllers: { omniauth_callbacks: "callbacks" }
devise_scope :user do
get 'sign_out', to: 'devise/sessions#destroy', as: :destroy_user_session
post 'sign_in', to: 'welcome#token_sign_in'
unauthenticated :user do
root to: 'welcome#index'
end
@ -18,17 +17,21 @@ Rails.application.routes.draw do
end
end
resources :users, only: [:show, :edit, :update, :index, :destroy] do
resources :users, only: [:show, :edit, :update] do
resources :orders, only: [:new, :create, :destroy]
member do
get 'quickpay' => 'users#quickpay'
get 'dagschotel/edit' => 'users#edit_dagschotel', as: 'edit_dagschotel'
get 'dagschotel/:product_id' => 'users#update_dagschotel', as: 'dagschotel'
end
end
resources :products, only: [:new, :create, :index, :edit, :update]
resources :stocks, only: [:new, :create]
resources :products, only: [:create, :index, :edit, :update] do
resources :barcodes, only: [:create, :show], shallow: true
collection do
get 'barcode' => 'products#barcode', as: :barcode
post 'barcode' => 'products#load_barcode', as: :load_barcode
end
end
get 'overview' => 'orders#overview', as: "orders"
end

View file

@ -16,6 +16,7 @@ development:
omniauth_client_secret: blargh
access_token: "token"
tab_api_key: "HriaktSIhRaB5CJzD71uLQ=="
koelkast_token: ""
test:
secret_key_base: 961437e28e7d6055ffaad9cf1f8d614354f57f10cb2d7601c9d6ede72a03b9c9535ad9e63507e3eb31252c4895970a63117493408f2e9a46c7a0c4a5a7836b81
@ -29,3 +30,4 @@ production:
omniauth_client_secret: ""
access_token: ""
tab_api_key: ""
koelkast_token: ""

View file

@ -0,0 +1,9 @@
class RemoveDeviseFieldsFromUsers < ActiveRecord::Migration
def change
remove_column :users, :sign_in_count, :integer
remove_column :users, :current_sign_in_at, :datetime
remove_column :users, :last_sign_in_at, :datetime
remove_column :users, :current_sign_in_ip, :string
remove_column :users, :last_sign_in_ip, :string
end
end

View file

@ -0,0 +1,10 @@
class AddBarcodeToProducts < ActiveRecord::Migration
def change
create_table :barcodes do |t|
t.references :product
t.string :code, index: true, null: false, default: ""
t.timestamps
end
end
end

View file

@ -11,7 +11,16 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20150917165758) do
ActiveRecord::Schema.define(version: 20150919091214) do
create_table "barcodes", force: :cascade do |t|
t.integer "product_id"
t.string "code", default: "", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "barcodes", ["code"], name: "index_barcodes_on_code"
create_table "delayed_jobs", force: :cascade do |t|
t.integer "priority", default: 0, null: false
@ -66,11 +75,6 @@ ActiveRecord::Schema.define(version: 20150917165758) do
t.datetime "created_at"
t.datetime "updated_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.boolean "admin"
t.integer "dagschotel_id"
t.string "avatar_file_name"

View file

@ -1,17 +1,40 @@
# from_barcode_products POST /products/barcode(.:format) products#from_barcode
# products GET /products(.:format) products#index
# POST /products(.:format) products#create
# new_product GET /products/new(.:format) products#new
# edit_product GET /products/:id/edit(.:format) products#edit
# product PATCH /products/:id(.:format) products#update
# PUT /products/:id(.:format) products#update
#
describe ProductsController, type: :controller do
before :each do
@admin = create :admin
sign_in @admin
end
#########
# NEW #
#########
describe 'GET new' do
it 'should render the form' do
get :new
expect(response).to render_template(:new)
expect(response).to have_http_status(200)
end
it 'should initialize a new product' do
get :new
expect(assigns(:product).class).to eq(Product)
expect(assigns(:product)).to_not be_persisted
end
end
##########
# POST #
##########
describe 'POST create' do
context 'successfull' do
it 'should create a product' do
@ -22,7 +45,7 @@ describe ProductsController, type: :controller do
it 'should redirect to index page' do
post :create, product: attributes_for(:product)
expect(response).to redirect_to action: :index
expect(response).to redirect_to action: :barcode
end
end
@ -35,11 +58,15 @@ describe ProductsController, type: :controller do
it 'should render form' do
post :create, product: attributes_for(:invalid_product)
expect(response).to render_template(:new)
expect(response).to render_template(:link)
end
end
end
###########
# INDEX #
###########
describe 'GET index' do
it 'should load all the products' do
product = create :product
@ -48,6 +75,10 @@ describe ProductsController, type: :controller do
end
end
##########
# EDIT #
##########
describe 'GET edit' do
before :each do
@product = create :product
@ -65,6 +96,10 @@ describe ProductsController, type: :controller do
end
end
############
# UPDATE #
############
describe 'PUT update' do
before :each do
@product = create :product
@ -75,12 +110,32 @@ describe ProductsController, type: :controller do
expect(assigns :product).to eq(@product)
end
context 'successful' do
it 'should update attributes' do
put :update, id: @product, product: { name: "new_product_name" }
expect(@product.reload.name).to eq("new_product_name")
end
end
context 'failed' do
it 'should not update attributes' do
old_attributes = @product.reload.attributes
old_attributes = @product.attributes
put :update, id: @product, product: attributes_for(:invalid_product)
expect(@product.reload.attributes).to eq(old_attributes)
end
end
end
##################
# FROM_BARCODE #
##################
describe 'POST from_barcode' do
it 'should return a product when barcode in database' do
product = create :product
bar = create :barcode, product: product
post :from_barcode, barcode: bar.code
expect(JSON.parse(response.body)["id"]).to eq(product.id)
end
end
end

View file

@ -1,5 +1,10 @@
require 'identicon'
require 'faker'
# quickpay_user GET /users/:id/quickpay(.:format) users#quickpay
# edit_dagschotel_user GET /users/:id/dagschotel/edit(.:format) users#edit_dagschotel
# edit_user GET /users/:id/edit(.:format) users#edit
# user GET /users/:id(.:format) users#show
# PATCH /users/:id(.:format) users#update
# PUT /users/:id(.:format) users#update
#
describe UsersController, type: :controller do
before :each do
@ -7,6 +12,10 @@ describe UsersController, type: :controller do
sign_in @user
end
##########
# SHOW #
##########
describe 'GET show' do
before :each do
get :show, id: @user
@ -22,6 +31,10 @@ describe UsersController, type: :controller do
end
end
##########
# EDIT #
##########
describe 'GET edit' do
before :each do
get :edit, id: @user
@ -36,6 +49,10 @@ describe UsersController, type: :controller do
end
end
############
# UPDATE #
############
describe 'PUT update' do
it 'should load the correct user' do
put :update, id: @user, user: attributes_for(:user)
@ -48,23 +65,18 @@ describe UsersController, type: :controller do
put :update, id: @user, user: { private: new_private }
expect(@user.reload.private).to be new_private
end
it 'should update dagschotel' do
product = create :product
put :update, id: @user, user: { dagschotel_id: product.id }
expect(@user.reload.dagschotel).to eq(product)
end
end
end
describe 'GET index' do
before :each do
get :index
end
it 'should load an array of all users' do
expect(assigns(:users)).to eq([@user])
end
it 'should render the correct template' do
expect(response).to render_template(:index)
expect(response).to have_http_status(200)
end
end
#####################
# EDIT_DAGSCHOTEL #
#####################
describe 'GET edit_dagschotel' do
it 'should render the page' do
@ -73,12 +85,4 @@ describe UsersController, type: :controller do
expect(response).to have_http_status(200)
end
end
describe 'GET update_dagschotel' do
it 'should update the dagschotel' do
product = create :product
get :update_dagschotel, id: @user, product_id: product
expect(@user.reload.dagschotel).to eq(product)
end
end
end

View file

@ -0,0 +1,17 @@
# == Schema Information
#
# Table name: barcodes
#
# id :integer not null, primary key
# product_id :integer
# code :string default(""), not null
# created_at :datetime
# updated_at :datetime
#
FactoryGirl.define do
factory :barcode do
product
sequence :code
end
end

View file

@ -6,11 +6,6 @@
# created_at :datetime
# updated_at :datetime
# remember_created_at :datetime
# sign_in_count :integer default("0"), not null
# current_sign_in_at :datetime
# last_sign_in_at :datetime
# current_sign_in_ip :string
# last_sign_in_ip :string
# admin :boolean
# dagschotel_id :integer
# avatar_file_name :string

View file

@ -5,37 +5,44 @@ describe User do
subject(:ability){ Ability.new(user) }
let(:user) { nil}
# Admin
describe 'as admin' do
let(:user) { create :admin }
it{ should be_able_to(:manage, Product.new) }
it{ should be_able_to(:manage, Order.new) }
it{ should be_able_to(:manage, OrderItem.new) }
it{ should be_able_to(:manage, Product.new) }
it{ should be_able_to(:manage, Stock.new) }
it{ should be_able_to(:manage, User.new) }
end
# Normal User
describe 'as normal user' do
let(:user) { create :user }
it{ should be_able_to(:read, Product.new) }
it{ should_not be_able_to(:manage, Product.new) }
it{ should be_able_to(:create, Order.new(user: user)) }
it{ should be_able_to(:delete, Order.new(user: user, created_at: (Rails.application.config.call_api_after - 1.minutes).ago)) }
it{ should_not be_able_to(:delete, Order.new(user: user, created_at: 10.minutes.ago)) }
it{ should_not be_able_to(:manage, Order.new) }
it{ should_not be_able_to(:create, Order.new) }
it{ should_not be_able_to(:update, Order.new) }
it{ should_not be_able_to(:manage, Stock.new) }
it{ should be_able_to(:read, Product.new) }
it{ should_not be_able_to(:delete, Product.new) }
it{ should_not be_able_to(:update, Product.new) }
it{ should_not be_able_to(:create, Stock.new) }
it{ should be_able_to(:manage, user) }
it{ should_not be_able_to(:manage, User.new) }
it{ should_not be_able_to(:create, User.new) }
it{ should_not be_able_to(:update, User.new) }
end
describe 'as koelkast' do
let(:user) { create :koelkast }
it{ should_not be_able_to(:manage, Product.new) }
it{ should be_able_to(:manage, Order.new) }
it{ should be_able_to(:manage, Order.new, user: create(:user)) }
it{ should_not be_able_to(:create, build(:order, user: create(:user, private: true))) }
it{ should_not be_able_to(:manage, Stock.new) }
it{ should_not be_able_to(:manage, User.new) }
end

View file

@ -0,0 +1,27 @@
describe Barcode do
before :each do
@barcode = create :barcode
end
it 'has a valid factory' do
expect(@barcode).to be_valid
end
############
# FIELDS #
############
describe 'fields' do
describe 'code' do
it 'should be present' do
@barcode.code = nil
expect(@barcode).to_not be_valid
end
it 'should be unique' do
barcode = build :barcode, code: @barcode.code
expect(barcode).to_not be_valid
end
end
end
end

View file

@ -14,14 +14,20 @@ describe OrderItem do
expect(order_item).to be_valid
end
describe 'validations' do
############
# FIELDS #
############
describe 'fields' do
before :each do
@order_item = create :order_item
end
it 'product should be present' do
@order_item.product = nil
expect(@order_item).to_not be_valid
describe 'product' do
it 'should be present' do
@order_item.product = nil
expect(@order_item).to_not be_valid
end
end
describe 'count' do
@ -34,10 +40,21 @@ describe OrderItem do
@order_item.count = -5
expect(@order_item).to_not be_valid
end
it 'should be less or equal to product stock' do
@order_item.count = @order_item.product.stock + 1
expect(@order_item).to_not be_valid
@order_item.count = @order_item.product.stock
expect(@order_item).to be_valid
end
end
end
describe 'product stock' do
###############
# CALLBACKS #
###############
describe 'stock change' do
before :each do
@product = create :product
@count = rand 10
@ -48,7 +65,7 @@ describe OrderItem do
expect{ @order_item.save }.to change{ @product.stock }.by(-@count)
end
it 'should increment on cancel' do
it 'should increment on destroy' do
@order_item.save
expect{ @order_item.destroy }.to change{ @product.stock }.by(@count)
end

View file

@ -20,16 +20,58 @@ describe Order do
expect(@order).to be_valid
end
describe 'price' do
it 'should be calculated from order_items' do
@order = build :order, products_count: 0
sum = (create_list :product, 1 + rand(10)).map do |p|
create(:order_item, order: @order, product: p, count: 1 + rand(5)) do |oi|
@order.order_items << oi
end
end.map{ |oi| oi.count * oi.product.price_cents }.sum
@order.valid?
expect(@order.price_cents).to eq(sum)
############
# FIELDS #
############
describe 'fields' do
describe 'user' do
it { Order.reflect_on_association(:user).macro.should eq(:belongs_to) }
it 'should be present' do
@order.user = nil
expect(@order).to_not be_valid
end
end
describe 'price_cents' do
it 'should be calculated from order_items' do
@order = build :order, products_count: 0
sum = (create_list :product, 1 + rand(10)).map do |p|
create(:order_item, order: @order, product: p, count: 1 + rand(5)) do |oi|
@order.order_items << oi
end
end.map{ |oi| oi.count * oi.product.price_cents }.sum
@order.save
expect(@order.price_cents).to eq(sum)
end
end
describe 'order_items' do
it 'should be validated' do
@order.order_items.build(count: -5)
expect(@order).to_not be_valid
end
end
describe 'products' do
it 'should be present' do
@order.products.clear
expect(@order).to_not be_valid
end
end
end
###############
# CALLBACKS #
###############
describe 'empty order_items' do
it 'should be removed' do
product = create :product
@order.order_items << create(:order_item, order: @order, product: product, count: 0)
@order.save
expect(@order.order_items.where(product: product)).to be_empty
end
end
end

View file

@ -26,23 +26,38 @@ describe Product do
expect(@product).to be_valid
end
describe 'validations' do
it 'name should be present' do
@product.name = ''
expect(@product).to_not be_valid
end
############
# FIELDS #
############
describe 'price' do
it 'should be positive' do
@product.price = -5
describe 'fields' do
describe 'name' do
it 'should be present' do
@product.name = nil
expect(@product).to_not be_valid
end
it 'should be saved correctly' do
@product.price = 1.20
@product.save
expect(@product.reload.price).to eq(1.20)
expect(@product.reload.price_cents).to eq(120)
it 'shold be unique' do
expect(build :product, name: @product.name).to_not be_valid
end
end
describe 'price_cents' do
it 'should be present' do
@product.price_cents = nil
expect(@product).to_not be_valid
end
it 'should be a number' do
@product.price_cents = "123abc"
expect(@product).to_not be_valid
end
it 'should be strict positive' do
@product.price = -5
expect(@product).to_not be_valid
@product.price = 0
expect(@product).to_not be_valid
end
end
@ -55,11 +70,13 @@ describe Product do
it 'should be positive' do
@product.stock = -5
expect(@product).to_not be_valid
@product.stock = 0
expect(@product).to be_valid
end
end
describe 'calories' do
it 'should not be present' do
it 'does not have to be present' do
@product.calories = nil
expect(@product).to be_valid
end
@ -70,9 +87,28 @@ describe Product do
end
end
it 'avatar should be present' do
@product.avatar = nil
expect(@product).to_not be_valid
describe 'avatar' do
it 'should be present' do
@product.avatar = nil
expect(@product).to_not be_valid
end
end
end
#############
# METHODS #
#############
describe 'price' do
it 'should read the correct value' do
expect(@product.price).to eq(@product.price_cents / 100.0)
end
it 'should write the correct value' do
@product.price = 1.5
@product.save
expect(@product.reload.price_cents).to eq(150)
end
end
end

View file

@ -6,11 +6,6 @@
# created_at :datetime
# updated_at :datetime
# remember_created_at :datetime
# sign_in_count :integer default("0"), not null
# current_sign_in_at :datetime
# last_sign_in_at :datetime
# current_sign_in_ip :string
# last_sign_in_ip :string
# admin :boolean
# dagschotel_id :integer
# avatar_file_name :string
@ -32,4 +27,23 @@ describe User do
it 'has a valid factory' do
expect(@user).to be_valid
end
############
# FIELDS #
############
describe 'fields' do
describe 'avatar' do
it 'should be present' do
@user.avatar = nil
expect(@user).to_not be_valid
end
end
describe 'orders_count' do
it 'should automatically cache the number of orders' do
expect{ create :order, user: @user }.to change{ @user.reload.orders_count }.by(1)
end
end
end
end