Rails 5.1, search scope within a group - ruby-on-rails

I have a little issue with a search scope.
Users can join groups by providing a token. User can also create spendings and they can share them in a group. We are in the show view of a group. This is where I loop through spendings that each user has in this group. It looks something like that :
<% group_spendings_paginate_search.each do |groupspending| %>
<%= groupspending.member.user.firstname %>
<%= groupspending.spending.title %>
<%= groupspending.spending.description %>
<%= '%.02f' % groupspending.spending.amount %>
<%= groupspending.spending.currency.symb %>
<%= groupspending.created_at.strftime("%d/%m/%Y") %>
<% end %>
The group_spendings_paginate_search comes from a helper which represent this :
def group_spendings_paginate_search
#search.scope.order('created_at DESC').paginate(:page => params[:page], :per_page => 10)
end
The search form looks like this and is just above the loop :
<%= form_tag group_path(#group), method: :get do %>
<%= date_field_tag "search[date_from]", #search.date_from %>
<%= date_field_tag "search[date_to]", #search.date_to %>
<%= select_tag "search[user_id]", options_from_collection_for_select(#group.users.all, :id, :firstname, params[:user_id]), include_blank: "All Users" %>
<%= select_tag "search[currency_id]", options_from_collection_for_select(Currency.all, :id, :name, params[:currency_id]), include_blank: "All Currencies" %>
<%= submit_tag "Search", name: nil %>
<% end %>
Note that here I had to go for Currency.all which is not the currencies the user are using. They may only use euro for example and here the all list is going to pop, which is also not very cool. Would prefer to show only the currencies the users are using according to the group and spending. But hey it's already complicated enough so for now I'll keep it simple and come back on that later. But if anybody has a idea, feel free.
My controller :
def show
#search = GroupspendingSearch.new(params[:search])
end
And finally my GroupspendingSearch.rb
class GroupspendingSearch
attr_reader :date_from, :date_to, :user_id, :currency_id
def initialize(params)
params ||= {}
#date_from = parsed_date(params[:date_from], 1.month.ago.to_date)
#date_to = parsed_date(params[:date_to], Date.tomorrow)
#user_id = params[:user_id]
#currency_id = params[:currency_id]
end
def scope
launch = Groupspending.where('groupspendings.created_at BETWEEN ? AND ?', #date_from, #date_to)
launch = launch.where(member_id: find_member) if find_member.exists?
launch = launch.where(spending_id: find_currency) if find_currency.exists?
launch
end
private
def parsed_date(date_string, default)
Date.parse(date_string)
rescue ArgumentError, TypeError
default
end
def find_member
Member.where(user_id: #user_id)
end
def find_currency
Spending.where(currency_id: #currency_id)
end
end
The scope as is is actually working. Only problem is that it goes for all the Groupspending and not #group.groupspendings. It means that if I go in any group I will see all the spendings and I want to avoid that of course. Also I had to be smart about how I find the user_id and the currency_id. Using the table Groupspending gives me only member_id and spending_id. With the group_id or token in this model everything would be way easier...
Basically I don't know how to specify in the scope to look for the #group if this makes any sense.
I thought about a couple of things. The first is in the controller and specify a other param like so #search = GroupspendingSearch.new(params[:search],#group) and add it in the model def initialize(params, group) but to be honnest I don't really know what I'm doing so yea...
I'm dry here, anybody to help ? Maybe I'm totally wrong here and there is a other approach.
Models relations (don't pay attention to link and notification, it's a other story ^^) :
class Currency < ApplicationRecord
has_many :spendings, dependent: :destroy
has_many :users, through: :spendings
end
class Group < ApplicationRecord
has_secure_token :auth_token
has_many :members, :dependent => :destroy
has_many :users, through: :members, source: :user
belongs_to :owner, class_name: "User"
has_many :links, through: :grouplinks
has_many :grouplinks, through: :members
has_many :spendings, through: :groupspendings
has_many :groupspendings, through: :members
has_many :notifications, dependent: :destroy
def to_param
auth_token
end
end
class Groupspending < ApplicationRecord
belongs_to :spending
belongs_to :member
end
class Member < ApplicationRecord
belongs_to :user
belongs_to :group
has_many :grouplinks, dependent: :destroy
has_many :groupspendings, dependent: :destroy
validates :user_id, :presence => true
validates :group_id, :presence => true
validates :user_id, :uniqueness => {:scope => [:user_id, :group_id]}
end
class Spending < ApplicationRecord
belongs_to :user
belongs_to :currency
has_many :groupspendings, dependent: :destroy
end
class User < ApplicationRecord
has_secure_password
has_many :spendings, dependent: :destroy
has_many :currencies, through: :spendings
has_many :links, dependent: :destroy
has_many :notifications, dependent: :destroy
has_many :members, :dependent => :destroy
has_many :groups, :through => :members
has_one :owned_group, foreign_key: "owner_id", class_name: "Group"
end
EDIT :
Well I just realized that actually the scope isn't correct... I look for member_id: find_member and this goes for all the member with a member_id: X. Which means that if a user is a member of many groups well... it shows multiple entries. I'm getting crazy ^^

Ok I think I got it but I want to be sure that there is no mistake or security issues...
Here is what I changed :
Helper
def group_spendings_paginate_search
m = Member.where(group_id: #group.id)
gs = Groupspending.where(member_id: m)
#search.scope.where(member_id: gs.all.map(&:member_id) ).order('created_at DESC').paginate(:page => params[:page], :per_page => 10)
end
I didn't change the view and didn't change the GroupspendingSearch either. With this new helper I retrieve only the members within the group which "blocks" the search query to that. I guess...
I don't know if it's the best, and I don't know if it's safe. In the URL if I try to change the user_id to someone else, if he/she is not part of the group it doesn't show up. So it looks ok. Of course I have to secure the show view by restricting only to the members of the group. But appart from that, would that be ok ?

Related

I am trying to implement a join table but my variables aren't saving properly. Best advice to refactor this?

I am trying to build out an application where users can post jobs and other users can accept those jobs and for the route to render "complete" after clicking accept job. I have built out a join table to host the multiple responder_users The problem is that my acceptances do not save. Here are my models:
class User < ApplicationRecord
has_secure_password
validates :username, presence: true
validates :email, presence: true
validates :email, uniqueness: true
has_many :acceptances
has_many :job_requests, :class_name => "Job", :foreign_key => "requestor_user_id"
has_many :job_responses, :class_name => "Job", :foreign_key => "responder_user_id"
end
class Job < ApplicationRecord
has_many :acceptances
belongs_to :requestor_user, :class_name => "User"
has_many :responder_user, :class_name => "User", through: :acceptances
end
class Acceptance < ApplicationRecord
belongs_to :user
belongs_to :job
end
And this is the update method that springs from the show page where the accept job is at.
def update
#job = Job.find(params[:id])
#acceptance = Acceptance.create(job_id: #job.id, responder_id: #current_user.id)
#job.acceptances << #acceptance
if #job.save
redirect_to jobs_path
else
render :show
end
end
Any help/ advice would be much appreciated!
I would suggest this one..
def update
#job = Job.find(params[:id])
#acceptance = #job.acceptances.new(job_id: #job.id, user: #current_user)
if #acceptance.save
redirect_to jobs_path
else
render :show
end
end
I find the wording very confusing. Applications might be better, a job has many applications and one application gets accepted. But I hope that helps.
I came through on the solution for those wracking their brains on something similar. https://www.theodinproject.com/paths/full-stack-ruby-on-rails/courses/ruby-on-rails/lessons/active-record-associations . This is a great link to further your knowledge hosted by the odin project. Basically, I arranged my models as followed:(job)
belongs_to :requestor_user, :class_name => "User"
has_many :acceptances, foreign_key: :job_response_id
has_many :responder_users, through: :acceptances, source: :responder
(user)
has_many :acceptances, foreign_key: :responder_id
has_many :job_requests, :class_name => "Job", :foreign_key => "requestor_user_id"
has_many :job_responses, through: :acceptances
(acceptance)
belongs_to :responder, class_name: "User"
belongs_to :job_response, class_name: "Job"
Essentially, the goal here was to figure out how to split two different groups of the same model(in the instance of User & job). From there I was able to host the acceptances in the join table. From there I was able to create a response, as well as a responder for the specific job in my update route.:
def update
#job = Job.find(params[:id])
#current_user.job_responses << #job
#job.responder_users << #current_user
if #job.save
redirect_to jobs_path
else
render :show
end
end
To show the completion, I simply added some logic in the view for the job:
<%if #acceptance%>
<h2>Completed!</h2>
<%else%>
<%= form_for(#job) do |f| %>
<%= f.submit "Accept Job" %>
<% end %>
<%end%>
Thank you for those who reached out!

Rails: Structuring rental orders & order form in ecommerce-like setting

I was wondering if someone could help me out with an application that has some ecommerce characteristics.
Context: Via the application a bike shop chain ('chains') can rent out
bikes ('bikes'),
by picking out a bike type such as mountainbike, city bike etc. ('bike_types) and
bike options, such as helmets etc. ('bike_options')
which are dependent on the individual bike store ('bike_stores')
this rental of the bikes & options will all be captured in an order ('orders')
the relationship between orders and bikes is many-to-many, therefore I created a table to bridge this ('order_bikes')
Final notes:
Before the rental process, the chain owner first created his/her (i) bike_stores, (ii) bike_types, (iii) bikes and (iv) bike_options, this part of the application is working. Therefore, he/she only needs to select bike_types/bikes/options out of the existing inventory previously created.
I limit the scope of the question by leaving out the bike_options, this was mainly to provide some context in order to understand the db schema build up.
Error message: Unpermitted parameter: :bike_id
Code:
models
class Order < ApplicationRecord
belongs_to :bike_store
has_many :bike_types, through: :bike_store
has_many :order_bikes, inverse_of: :order, dependent: :destroy
accepts_nested_attributes_for :order_bikes, allow_destroy: true
end
class OrderBike < ApplicationRecord
belongs_to :bike
belongs_to :order
accepts_nested_attributes_for :bike
end
class Bike < ApplicationRecord
belongs_to :bike_type
validates :name, presence: true
has_many :order_bikes
has_many :orders, through: :order_bikes
end
class BikeType < ApplicationRecord
belongs_to :bike_store
has_many :bikes, dependent: :destroy
accepts_nested_attributes_for :bikes, allow_destroy: true
has_many :bike_options, dependent: :destroy
accepts_nested_attributes_for :bike_options, allow_destroy: true
validates :name, :bike_count, presence: true
end
class BikeStore < ApplicationRecord
has_many :bike_types, dependent: :destroy
has_many :orders, dependent: :destroy
end
Order controller
class OrdersController < ApplicationController
def new
#bike_store = BikeStore.find(params[:bike_store_id])
#order = Order.new
#order.order_bikes.build
#bike_type_list = #bike_store.bike_types
end
def create
#order = Order.new(order_params)
#bike_store = BikeStore.find(params[:bike_store_id])
#order.bike_store = #bike_store
#order.save
redirect_to root_path
end
private
def order_params
params.require(:order).permit(:arrival, :departure,
order_bikes_attributes: [:id, :bike_quantity, :_destroy,
bikes_attributes: [:id, :name,
bike_types_attributes: [:id, :name]]])
end
end
view
<%= simple_form_for [#bike_store, #order] do |f|%>
<%= f.simple_fields_for :order_bikes do |order_bike| %>
<%= order_bike.input :bike_quantity %>
<%= order_bike.association :bike %>
<% end %>
<%= f.input :arrival %>
<%= f.input :departure %>
<%= f.submit %>
<% end %>
If you check coed from simple form here, you will see what actually method association does.
def association(association, options = {}, &block)
# ... simple form code here ...
attribute = build_association_attribute(reflection, association, options)
input(attribute, options.merge(reflection: reflection))
end
We are interested in build_association_attribute method call. here
def build_association_attribute(reflection, association, options)
case reflection.macro
when :belongs_to
(reflection.respond_to?(:options) && reflection.options[:foreign_key]) || :"#{reflection.name}_id"
# ... the rest of code ...
end
end
Your order bike model has belongs_to :bike association. So when you call order_bike.association :bike it builds :bike_id attribute in your form. If you check params hash that comes to your controller, I believe you'll see that attribute coming from your view.
I added bike_id to permitted parameters. I hope it will fix your problem..
def order_params
params.require(:order).permit(:arrival, :departure,
order_bikes_attributes: [:id, :bike_id, :bike_quantity, :_destroy,
bikes_attributes: [:id, :name,
bike_types_attributes: [:id, :name]]])
end

Rails poll app check if vote count for option has reached a value

I am creating an app to poll users. I started from this tutorial and modified it. I also posted another question regarding it here.
Each user has a vote weight. So far users can vote and vote count (calculated based on vote weight of the user) for each option is displayed.
What I am trying to do now is to set a condition that checks if any of the options for that poll has reached 50%.
vote.rb
class Vote < ApplicationRecord
belongs_to :user
belongs_to :vote_option
end
vote_option.rb
class VoteOption < ApplicationRecord
belongs_to :poll
validates :title, presence: true
has_many :users, through: :votes
has_many :votes, dependent: :destroy
def get_vote_count
Vote.joins(:vote_option).joins(:user).where("vote_options.id = #{self.id}").sum(:vote_weight)
end
end
poll.rb
class Poll < ApplicationRecord
validates :question, presence: true
validates :division, presence: true
validates :open_date, presence: true
validates :close_date, presence: true
before_save :set_position
set_sortable :sort, without_updating_timestamps: true
has_many :comments, as: :commentable
has_many :votes, :through => :vote_options
has_many :vote_options, dependent: :destroy
belongs_to :division
belongs_to :user
has_and_belongs_to_many :users
accepts_nested_attributes_for :vote_options, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :users
def normalized_votes_for(option)
votes_summary == 0 ? 0 : (option.get_vote_count.to_f / votes_summary) * 100
end
def votes_summary
vote_options.inject(0) {|summary, option| summary + option.get_vote_count}
end
end
polls_helper.rb
module PollsHelper
def visualize_votes_for(option)
content_tag :div, class: 'progress' do
content_tag :div, class: 'progress-bar',
style: "width: #{option.poll.normalized_votes_for(option)}%" do
"#{option.get_vote_count}"
end
end
end
end
polls_controller show
def show
#poll = Poll.includes(:vote_options).find_by_id(params[:id])
#vote = Vote.find_by_id(params[:id])
#users = #poll.users
#comments = Comment.with_details.all
end
So on polls/show.html.erb I'd need something like:
<% if option.get_vote_count.any > 50 %>
do something
<% end %>
How can I fix the code above to have it working? And what should I add on my polls_controller under show action?
Thanks in advance.
This was the solution. Thanks to #Ash to point me on right direction:
<% #poll.vote_options.each do |option| %>
<% if option.get_vote_count > 50 %>
<h4> Condition met! </h4>
<% end %>
<% end %>

Rails 5.1, sum by currency with nested resources

I have a problem with a SUM calculation. I have multiple currency in a table, I have users, I have groups and I have a bets table. The user can join a group via member (user_id and group_id). And the user can share his bet via groupbet (bet_id and member_id). I don't enter in detail here because all this works properly.
I would like to do some SUM calculation in the group view (show). This is where I'd like to display the total amount of money the members of the group spend for there bet. I also would like to use chartkick in the mix to show some nice graph.
This is what I have so far
My controller :
m = Member.where(group_id: #group.id)
gs = Groupbet.where(member_id: m)
#tasks = gs.joins(:bet).select("bets.*, sum(amount) as total")
#amount = gs.joins(:bet).
select(:symb, 'bets.*, sum(amount) AS amount').
where(bet_id: { id: #current_user.bets.all.map(&:id) }).
group(:symb)
#tasks works but SUM all the spendings from the group, that's cool, but I have several currencies so the amount is not correct. This is why I came up with #amount which in the view :
<% #amount.each do |amount| %>
<%= '%.02f' % "#{amount.amount}" %>
<%= "#{amount.symb}" %>
<% end %>
display nothing at all.
the :symb is one of my Currency column btw. In currency I have name and symb, where symbole is this : € or $ for example. In my spending table I have a column amount and the currency_id.
My relations :
class Currency < ApplicationRecord
has_many :bets, dependent: :destroy
has_many :users, through: :bets
end
class Group < ApplicationRecord
has_secure_token :auth_token
has_many :members, :dependent => :destroy
has_many :users, through: :members, source: :user
belongs_to :owner, class_name: "User"
has_many :links, through: :grouplinks
has_many :grouplinks, through: :members
has_many :bets, through: :groupbets
has_many :groupbets, through: :members
has_many :notifications, dependent: :destroy
def to_param
auth_token
end
end
class Groupbet < ApplicationRecord
belongs_to :bet
belongs_to :member
end
class Member < ApplicationRecord
belongs_to :user
belongs_to :group
has_many :grouplinks, dependent: :destroy
has_many :groupbets, dependent: :destroy
validates :user_id, :presence => true
validates :group_id, :presence => true
validates :user_id, :uniqueness => {:scope => [:user_id, :group_id]}
end
class Bet < ApplicationRecord
belongs_to :user
belongs_to :currency
has_many :groupbets, dependent: :destroy
end
class User < ApplicationRecord
has_secure_password
has_many :bets, dependent: :destroy
has_many :currencies, through: :bets
has_many :links, dependent: :destroy
has_many :notifications, dependent: :destroy
has_many :members, :dependent => :destroy
has_many :groups, :through => :members
has_one :owned_group, foreign_key: "owner_id", class_name: "Group"
end
Links has nothing to do here but yeah...
I'm also using groupdate and for chartkick I don't really know because when a user creates a bet he can actually go on his profile and check all the bets he did, as well in a chart. To display this chart, which is working, I'm doing like so :
My helper
def expenses_bet_amount_per_month
#current_user.bets.all.joins(:currency).group('currencies.symb').group_by_month(:date, format: "%B %Y").sum(:amount)
end
date is the date when the user create the bet. Don't ask why I didn't use created_at, doesn't matter :)
And my view :
<%= column_chart expenses_bet_amount_per_month %>
That works. For one single user. But for the group I don't know how I could make that happen.
If anyone can save the situation ? Thank you very much
EDIT :
The code for the single user :
def sums_by_currency_total
Currency.joins(:bets).
select(:symb, 'SUM(bets.amount) AS amount').
where(bets: { id: user_bets_total.map(&:id) }).
group(:symb)
end
def user_spendings_total
user.bets.all
end
That works for one user. Display the SUM from all bets per currency. But for a group ? I don't know
Ok people I managed !!!!
I share the code in case anybody is interested :
def sums_by_currency_group
m = Member.where(group_id: #group.id)
gs = Groupbet.where(member_id: m)
Currency.joins(:bets)
.select(:symb, 'SUM(bets.amount) AS amount')
.where(bets: { id: gs.all.map(&:bet_id) })
.group(:symb)
end
In the view :
<% sums_by_currency_group.each do |sum| %>
<%= '%.02f' % "#{sum.amount}" %> <%= "#{sum.symb}" %>
<% end %>
And there we go. All the SUM from the bets that are shared in each group.
I also solved the problem for chart kick :
def bets_amount_per_month_group
m = Member.where(group_id: #group.id)
gs = Groupbet.where(member_id: m)
Currency.joins(:bets)
.where(bets: { id: gs.all.map(&:bet_id) })
.group('currencies.symb')
.group_by_month(:date, format: "%B %Y")
.sum(:amount)
end
In the view :
<%= column_chart bets_amount_per_month_group %>
Et voila ! Nice chart of all the bets shared in the group. I used the same method as the calculation and it worked !! Olé !!

How to add quantity to a rails form (create n records when submitting)?

I have a library-like booking system. I want to make a form for adding books in stock, allowing the user to choose a book and choose a library (both are collection_select). Book has a many-to-many relationship with Library, through the stock_items table.
What I can't figure out is how can bring in the quantity, so that the user can add a number of instances of the same book to a chosen university. How should I approach implementing this quantity-type feature. It should create a chosen amount of records in the join table.
Here's my form (currently creates only 1 instance at a time):
<%= form_for(#item) do |f| %>
<%= f.label :choose_book %>
<%= f.collection_select(:book_id, Book.all, :id, :name, prompt: true) %>
<%= f.label :choose_library %>
<%= f.collection_select(:library_id, Library.all, :id, :name, prompt: true) %>
<%= f.submit "Add item in stock", class: "btn btn-info" %>
<% end %>
StockItem model
class StockItem < ActiveRecord::Base
belongs_to :library
belongs_to :book
has_many :bookings, foreign_key: :stock_id, dependent: :destroy
validates :availability, presence: true
validates :library_id, presence: true
end
Library model
class Library < ActiveRecord::Base
has_many :stock_items
has_many :books, through: :stock_items
end
Book model
class Book < ActiveRecord::Base
validates :year_of_publication, presence: true, length: { maximum: 4 }
validates :description, presence: true, length: { minimum: 10 }
validates :name, presence: true
has_many :stock_items, dependent: :destroy
has_many :libraries, through: :stock_items
has_many :contributions, dependent: :destroy
has_many :authors, through: :contributions
has_many :bookings, through: :stock_items
has_many :book_images, dependent: :destroy
accepts_nested_attributes_for :book_images
accepts_nested_attributes_for :authors
accepts_nested_attributes_for :libraries
accepts_nested_attributes_for :stock_items
accepts_nested_attributes_for :contributions
validates :name, presence: true
end
A bit of the StockItemsController
def create
#item = StockItem.new(item_params)
if #item.save
flash[:success] = "Item added to stock"
redirect_to stock_items_path
else
flash[:danger] = "Item has not been added to stock!"
render 'new'
end
end
def new
#item = StockItem.new
end
private
def item_params
params.require(:stock_item).permit(:library_id, :book_id, :availability)
end
I think your answer is systemic, rather than syntaxic, meaning you have to consider your system rather than the specific action syntax
--
M-to-M
You specifically need to look at the many-to-many association you've deployed here
Your population of the StockItem table is basically a way to create a collection for each library, and so you should look at the many collection based methods which ActiveRecord provides (specifically << and .delete)
Bottom line is that instead of creating new records for your StockItem model, I would just add to the collection of both library - giving you the files you need to make it work
--
Collection
As you're using has_many :through, you can add multiple instances of the same record to your collection (as opposed to has_and_belongs_to_many, which only permits single instances of records, as there is no primary_key)
This means you'll be able to just add books individually to the collection, and then use the sum method of SQL to calculate the quantity:
#app/controllers/stock_items_controller.rb
class StockItemsController < ApplicationController
def create
#library = Library.find params[:stock_item][:library_id]
#book = Book.find params[:stock_item][:book_id]
#library << #book
end
end
--
Quantity
This is opposed to the idea that you can add a qty attribute to your StockItem model, and then use the increment! method:
#app/controllers/stock_items_controller.rb
Class StockItemsController < ApplicationController
def create
#item = StockItem.find_by(library_id: params[:stock_item][:library_id],book_id: params[:stock_item][:book_id])
#item ? #item.increment!(:qty) : StockItem.create(stockitem_params)
end
end

Resources