I have a functioning, self built e-com web app, but right now the app assumes we have infinite quantity.
It uses line_items and product models.
I am going to add stock_QTY as an attribute to the product
For items that don't have any variants (sizes, colors etc.), the line_item will be created if and stock_QTY is greater than one.
I'm not sure how to deal with sizes though.
Should I create different Products? IE:
Shirt.create (name:"small green shirt", color:"green", size:S, stock_QTY:4)
Shirt.create (name:"medium green shirt", color:"green", size:M, stock_QTY:6)
Shirt.create (name:"large green shirt", color: "green", size:L, stock_QTY:1)
This seems repetitive, but at least the stock QTY can have some independence. Is there a way to create only one shirt record, with variants, and allow them to have different sizes?
Ideally I'd like
Shirt.create(name:"shirt", colors:['red', 'blue', 'green'], sizes: ['s','m',l'])
and then be able to do
Shirt.where(color => "green").where(size => "L").stock_QTY
=> X number
Shirt.where(color => "green").where(size => "M").stock_QTY
=> Y number
This way I have one model, but it can store different quantities depending on the scope of the variants.
Let me know if this is unclear.
Thanks!
Update
Product.rb
require 'file_size_validator'
class Product < ActiveRecord::Base
has_many :line_items
before_destroy :ensure_not_referenced_by_any_line_item
mount_uploader :image, ImageUploader
validates :price, :numericality => {:greater_than_or_equal_to => 0.01}
validates :title, :uniqueness => true
def to_param
"#{id}_#{permalink}"
end
private
# ensure that there are no line items referencing this product
def ensure_not_referenced_by_any_line_item
if line_items.empty?
return true
else
errors.add(:base, 'Line Items present')
return false
end
end
end
Here is my Product as it is now.
from seeds.rb
Product.create!([
{
:title => "Liaisons Shirt Green",
:description => "",
:has_size => true,
:price => 24.99,
:permalink => "shirt",
:weight => 16.00,
:units => 1.00,
:image => (File.open(File.join(Rails.root, "app/assets/images/dev7logo.png")))
}
])
So, my advice is to improve the DB schema to make it more flexible and scalable ;)
Define the Size and Color models (2 new tables), make your actual Product model the BaseProduct model (just renaming the table) and finally create the Product model (new table) which will have 3 external keys (base_product_id, color_id and size_id) and of course the stock_qty field to define all possible configurations with the minimal repetition of information :)!
Just a little help, you're final classes schema should be like:
class Color < ActiveRecord::Base
end
class Size < ActiveRecord::Base
end
class BaseProduct < ActiveRecord::Base
# This will have almost all fields from your actual Product class
end
class Product < ActiveRecord::Base
# Ternary table between Color, Size and BaseProduct
end
I'm omitting all associations because I like the idea you succeed on your own, but if you need, please just ask :)
This will allows you to do BaseProduct queries like:
base_product.colors
base_product.sizes
product.base_product # retrieve the base_product from a product
and to keep trace of the quantities:
product.stock_qty
product.color
product.size # size and color are unique for a specific product
You can also create some helper method to make the creation process similar to the one you'd like to have (as shown in your question).
Well I understand the approaches you wanted to deal with. Pretty easy business logic if I understand correctly. So you wanted the following things If I get you correctly:
You have so many products
You want to add stock count record
You wanted to validate the product for selling (line items for cart) if the product available
You need to ensure if the product is already in customer's cart when you are deleting that.
So I assumed you already added the stock_qty columns.
Now you need to ensure if the product is available to be added in your cart.
So you need to write your validation in your line_item modem.
class LineItem < ActiveRecord::Base
# other business logics are here
belongs_to :product
before_validation :check_if_product_available
def check_if_product_available
# you will find your product from controller, model should be responsible to perform business
# decision on them.
if !self.try(:product).nil? && self.product.stock_qty < 1
errors.add(:product, 'This product is not available in the stock')
return false
end
end
end
This is the approach I believe is the valid way to do. And moreover, rather saving variants in same product model, I would suggest consider designing your model more efficiently with separate variant model or you can utilize the power of self association.
I hope this will help you. Let me know if I miss anything or miss interpret your problem.
Thanks
Related
I'm trying to build a reservation system where a customer can reserve a minibus. I've been able to get the all the data so a booking can be made.
I'm trying to avoid another user to reserve the minibus for the same day. I'm not to sure how to go about it as in new to ruby on rails.
In my reservation.rb I've got
belongs_to :vehicle
belongs_to :user
In my user.rb and vehicle.rb I've got
has_many :reservation
In my reservation controller I've got
def new
#vehicle = Vehicle.find(params[:vehicle_id])
#reservation = Reservation.new(user_id: User.find(session[:user_id]).id)
#reservation.vehicle_id = #vehicle.id
end
would I use validation to stop double reservations?
would it be something like in my reservation.rb
validates :vehicle_id, :startDate, :uniqueness => { :message => " minibus already reserved"}
Although the above will only allow the vehicle to be reserved.
Any help will be much appreciated!
As you already figured out you cannot use Rails' built-in uniqueness validator to validate that two ranges do not overlap.
You will have to build a custom validation to check this. A condition that checks if two time or date ranges A and B overlap is quite simple. Have a look at this image.
A: |-----|
B1: |-----|
B2: |---------|
B3: |-----|
C1: |-|
C2: |-|
A and B overlap if B.start < A.end && B.end > A.start
Add the following to your model:
# app/models/reservation.rb
validate :reservations_must_not_overlap
private
def reservations_must_not_overlap
return if self
.class
.where.not(id: id)
.where(vehicle_id: vehicle_id)
.where('start_date < ? AND end_date > ?', end_date, start_date)
.none?
errors.add(:base, 'Overlapping reservation exists')
end
Some notes:
You might need to adjust the naming of the database columns and the attributes names because I wasn't sure if it was just a typo or if you use names not following Ruby conventions.
Furthermore, you might need <= and >= (instead of < and >), depending on your definition of start and end.
Moving the condition into a named scope is a good idea and will improve readability
You're gonna want to use the uniqueness validator lie you're already doing but use the scope option.
The example they give on that page is pretty similar to your use case:
class Holiday < ApplicationRecord
validates :name, uniqueness: { scope: :year,
message: "should happen once per year" }
end
As to which column you should validate, it doesn't really matter. Since the uniqueness scope is going to be all three columns, it can be any of them:
validates :vehicle_id, uniqueness, { scope: [:startDate, user_id], message: "your message" }
You should also add indexes to the database as described here (this question is very similar to yours by the way).
I have a model called Person that the user selects five personality Traits for. However, the order they pick them for matters (they are choosing most descriptive to least descriptive).
I know how to create a join table with a poison an do ordering that way. I'm using acts_as_list as well.
But I can't seem to find any help on, is how to create a way for the user of my app to set the order of the traits. That is I want to have say five select boxes on in the HTML and have them pick each one, and use something like jQuery UI Sortable to allow them to move them around if they like.
Here is a basic idea of my models (simplified for the purpose of just getting the concept).
class Person < ActiveRecord::Base
has_many :personalizations
has_many :traits, :through => :personalizations, :order => 'personalizations.position'
end
class Personalization < ActiveRecord::Base
belongs_to :person
belongs_to :trait
end
class Trait < ActiveRecord::Base
has_many :persons
has_many :persons, :through => :personalizations
end
I just have no idea how to get positioning working in my view/controller, so that when submitting the form it knows which trait goes where in the list.
After a lot of research I'll post my results up to help someone else encase they need to have list of records attached to a model via many-to-many through relationship with being able to sort the choices in the view.
Ryan Bates has a great screencast on doing sorting with existing records: http://railscasts.com/episodes/147-sortable-lists-revised
However in my case I needed to do sorting before my Person model existed.
I can easily add an association field using builder or simple_form_for makes this even easier. The result will be params contains the attribute trait_ids (since my Person has_many Traits) for each association field:
#view code (very basic example)
<%= simple_form_for #character do |f| %>
<%= (1..5).each do |i| %>
<%= f.association :traits %>
<% end %>
<% end %>
#yaml debug output
trait_ids:
- ''
- '1'
- ''
- '2'
- ''
- '3'
- ''
- '4'
- ''
- '5'
So then the question is will the order of the elements in the DOM be respected whenever the form is submitted. Specially if I implement jQuery UI draggable? I found this Will data order in post form be the same to it in web form? and I agree with the answer. As I suspected, too risky to assume the order will always be preserved. Could lead to a bug down the line even if it works in all browsers now.
Therefore after much looking I've concluded jQuery is a good solution. Along with a virtual attribute in rails to handle the custom output. After a lot of testing I gave up on using acts_as_list for what I am trying to do.
To explain this posted solution a bit. Essentially I cache changes to a virtual property. Then if that cache is set (changes were made) I verify they have selected five traits. For my purposes I am preserving the invalid/null choices so that if validation fails when they go back to the view the order will remain the same (e.g. if they skipped the middle select boxes).
Then an after_save call adds these changes to the database. Any error in after_save is still wrapped in a transaction so if any part were to error out no changes will be made. It was easiest therefore to just delete all the endowments and save the new ones (there might be a better choice here, not sure).
class Person < ActiveRecord::Base
attr_accessible :name, :ordered_traits
has_many :endowments
has_many :traits, :through => :endowments, :order => "endowments.position"
validate :verify_list_of_traits
after_save :save_endowments
def verify_list_of_traits
return true if #trait_cache.nil?
check_list = #trait_cache.compact
if check_list.nil? or check_list.size != 5
errors.add(:ordered_traits, 'must select five traits')
elsif check_list.uniq{|trait| trait.id}.size != 5
errors.add(:ordered_traits, 'traits must be unique')
end
end
def ordered_traits
list = #trait_cache unless #trait_cache.nil?
list ||= self.traits
#preserve the nil (invalid) values with '-1' placeholders
list.map {|trait| trait.nil?? '-1' : trait.id }.join(",")
end
def ordered_traits=(val)
#trait_cache = ids.split(',').map { |id| Trait.find_by_id(id) }
end
def save_endowments
return if #trait_cache.nil?
self.endowments.each { |t| t.destroy }
i = 1
for new_trait in #trait_cache
self.endowments.create!(:trait => new_trait, :position => i)
i += 1
end
end
Then with simple form I add a hidden field
<%= f.hidden :ordered_traits %>
I use jQuery to move the error and hint spans to the correct location inside
the div of five select boxes I build. Then I had a submit event handler on the form and convert the selection from the five text boxes in the order they are in the DOM to an array of comma separated numbers and set the value on the hidden field.
For completeness here is the other classes:
class Trait < ActiveRecord::Base
attr_accessible :title
has_many :endowments
has_many :people, :through => :endowments
end
class Endowment < ActiveRecord::Base
attr_accessible :person, :trait, :position
belongs_to :person
belongs_to :trait
end
I currently have the following controller method in a Rails app:
def index
#entries = []
#entries << QuickPost.where(:user_id.in => current_user.followees.map(&:ff_id) << current_user.id)
#entries << Infographic.where(:user_id.in => current_user.followees.map(&:ff_id) << current_user.id)
#entries.flatten!.sort!{ |a,b| b.created_at <=> a.created_at }
#entries = Kaminari.paginate_array(#entries).page(params[:page]).per(10)
end
I realise this is terribly inefficient so I'm looking for a better way to achieve the same goal but I'm new to MongoDB and wondering what the best solution would be.
Is there a way to make a sorted limit() query or a MapReduce function in MongoDB across two collections? I'm guessing there isn't but it would certainly save a lot of effort in this case!
I'm currently thinking I have two options:
Create a master 'StreamEntry' type model and have both Infographic and QuickPost inherit from that so that both data types are stored on the same collection. The issue with this is that I have existing data and I don't know how to move it from the old collections to the new.
Create a separate Stream/ActivityStream model using something like Streama (https://github.com/christospappas/streama). The issues I can see here is that it would require a fair bit of upfront work and due to privacy settings and editing/removal of items the stream would need to be rebuilt often.
Are there options I have overlooked? Am I over-engineering with the above options? What sort of best practices are there for this type of situation?
Any info would be greatly appreciated, I'm really liking MongoDB so far and want to avoid falling into pitfalls like this in the future. Thanks.
The inherit solution is fine, but when the inherited models are close.
For example :
class Post < BasePost
field :body, type: String
end
class QuickPost < BasePost
end
class BasePost
field :title, type: String
field :created_at, type: Time
end
But when the models grows, or are too different, your second solution is better.
class Activity
include Mongoid::Document
paginates_per 20
field :occurred_at, :type => Time, :default => nil
validates_presence_of :occurred_at
belongs_to :user
belongs_to :quick_post
belongs_to :infographic
default_scope desc(:occurred_at)
end
and for example :
class QuickPost
include Mongoid::Document
has_one :activity, :dependent => :destroy
end
The dependant destroy make the activity destroyed when the QuickPost is destroyed. You can use has_many and adapt.
And to create the activities, you can create an observer :
class ActivityObserver < Mongoid::Observer
observe :quick_post, :infographic
def after_save(record)
if record.is_a? QuickPost
if record.new_record?
activity = record.build_activity
activity.user = record.user
# stuff when it is new
else
activity = record.activity
end
activity.occurred_at = record.occurred_at
# common stuff
activity.save
end
end
end
So let's say you have
line_items
and line_items belong to a make and a model
a make has many models and line items
a model belongs to a make
For the bare example idea LineItem.new(:make => "Apple", :model => "Mac Book Pro")
When creating a LinteItem you want a text_field box for a make and a model. Makes and models shouldn't exist more than once.
So I used the following implementation:
before_save :find_or_create_make, :if => Proc.new {|line_item| line_item.make_title.present? }
before_save :find_or_create_model
def find_or_create_make
make = Make.find_or_create_by_title(self.make_title)
self.make = make
end
def find_or_create_model
model = Model.find_or_create_by_title(self.model_title) {|u| u.make = self.make}
self.model = model
end
However using this method means I have to run custom validations instead of a #validates_presence_of :make due to the associations happening off a virtual attribute
validate :require_make_or_make_title, :require_model_or_model_title
def require_make_or_make_title
errors.add_to_base("Must enter a make") unless (self.make || self.make_title)
end
def require_model_or_model_title
errors.add_to_base("Must enter a model") unless (self.model || self.model_title)
end
Meh, this is starting to suck. Now where it really sucks is editing with forms. Considering my form fields are a partial, my edit is rendering the same form as new. This means that :make_title and :model_title are blank on the form.
I'm not really sure what the best way to rectify the immediately above problem is, which was the final turning point on me thinking this needs to be refactored entirely.
If anyone can provide any feedback that would be great.
Thanks!
I don't think line_items should belong to a make, they should only belong to a model. And a model should have many line items. A make could have many line items through a model. You are missing a couple of methods to have your fields appear.
class LineItem
belongs_to :model
after_save :connect_model_and_make
def model_title
self.model.title
end
def model_title=(value)
self.model = Model.find_or_create_by_title(value)
end
def make_title
self.model.make.title
end
def make_title=(value)
#make = Make.find_or_create_by_title(value)
end
def connect_model_and_make
self.model.make = #make
end
end
class Model
has_many :line_items
belongs_to :make
end
class Make
has_many :models
has_many :line_items, :through => :models
end
It's really not that bad, there's just not super easy way to do it. I hope you put an autocomplete on those text fields at some point.
I have Labellings which belong to Emails and Labels.
Each labelling must be unique to the email/label pair - so an email can only be labelled 'test' once.
I'm doing this with validates_uniqueness_of :label_id, :scope => :email_id. This works as expected.
When I am labelling emails, I want to add the labelling if it is unique, and do nothing if the email is already labelled with that label.
I don't want to duplicate the validation functionality around my app with something like:
email.labels << label unless email.labels.include?(label)
Is it possible to ensure each labelling has a unique email_id/label_id pair without having to check it manually or handle exceptions?
I haven't tested it, but you can probably override << in the association proxy, something like:
class Email < ActiveRecord::Base
has_many :labelings
has_many :labels, :through => :labelings do
def <<(label)
unless proxy_owner.labels.include?(label)
proxy_owner.labelings << Labeling.new(:email => proxy_owner, :label => label)
end
end
end
end