I have a Project model that has_many: Tasks each Task has_many :users, through: :userAssociations so one person can work on a task but in the case of a meeting or any collaboration many users can work on the same task.
Tasks have a column: t.integer "time" which allows users to state in minutes the time the task took.
In the Show method of the Project, I'd like to be able to sum the total time that the project has taken.
If not for the fact that a Task can have many users through the userAssociations model, I think this would be very easy: #project.tasks.sum(:time) but that won't double count the time if multiple users were working on the task.
Since originally posting this question I was able to get it working with this helper:
def project_time_spent
#timeSum = 0;
#tasks.each do |tsk|
#userTask = UserTask.where(task_id: tsk.id);
if #userTask.count(:id) > 1
#timeSum += tsk.time * #userTask.count(:id)
else
#timeSum += tsk.time
end
end
return #timeSum
end
But this doesn't feel very "Railsy". Do any Rails gurus recommend a cleaner approach? Am I missing out on an easy method?
You can do it a little easier than that since a task has many users. I think you can also use inject here.
def project_time_spent
#tasks.inject { |sum, tsk| sum + (tsk.time * tsk.users.count) }
end
You can include an instance method in your Project model:
def time_spent
self.tasks.inject { |sum, tsk| sum + (tsk.time * tsk.users.count) }
end
I wouldn't bother checking for count > 1. The savings getting rid of the multiply are minuscule.
Related
I'm having trouble trying to isolate the next/previous record in a collection. I'm self-taught and relatively new to Rails/coding.
I have a Goal class which has many GoalTasks.
GoalTask has taskduedate. I want to be able to cycle next/previous on the goal_tasks, based on their taskduedate.
The issue is that a task due date is just when the task is due to be completed, but it can be set at any time and may not be in sequential order so that I don't know what else to order it by to correctly cycle through it.
I have created an array of goal_tasks to identify which one is currently being viewed (e.g. Task: 3/20), so I could use that to go to the next one, I think there might be a solution here, but it feels wrong to handle it in the view/controller?
I've tried the below solution from stackoverflow, but it doesn't handle the fact that I have multiple goal_tasks due on the same day, if I click next it just goes to the next day that goal_tasks are due. e.g. if I have three tasks due today and I'm on the first one and click next, it will just skip over the other two for today.
I then tried to add the >= (displayed below) to try and pull the next task (including those on the same day), and I've tried to ignore the current task by doing where created_at is not the same as the current goal_task and where.not, but I haven't managed to successfully get it to cycle the way I want it to, and I imagine there's a better solution.
GoalTasksController:
def show
#all_tasks_ordered_due_date_desc = #goal.goal_tasks.order('taskduedate ASC', 'id ASC')
end
show.html.erb:
Task: <%= #all_tasks_ordered_due_date_desc.find_index(#goal_task) +1 %> /
<%= #goal.goal_tasks.count%>
GoalTask.rb
scope :next_task, lambda {|taskduedate| where('taskduedate >= ?', taskduedate).order('id ASC') }
scope :last_task, lambda {|taskduedate| where('taskduedate <= ?', taskduedate).order('id DESC') }
def next_goal_task
goal.goal_tasks.next_task(self.taskduedate).first
end
Thanks
I used the method found here: Rails 5: ActiveRecord collection index_by
Which meant adding a default scope and changing GoalTask.rb to:
default_scope { order('taskduedate ASC') }
def next_goal_task
index = goal.goal_tasks.index self
goal.goal_tasks[index + 1]
end
def last_goal_task
index = goal.goal_tasks.index self
goal.goal_tasks[index -1]
end
I've built a RoR app and implemented a simple booking system. The user is able to look for a space and can book it per day or per hour.
Everything works well, but I would now like to make the user able to look for a space depending on its availability.
I want to user to be able to select a start/end date and a start/end time and to show only spaces that don't have any booking included in this period.
I am using pg search at the moment to look for a space by category and location, but I have no idea how to implement a search by date and time, as it uses a different logic.
I've tried to do it by hand by creating an array of bookings for each space so I could compare it with the params, but it sounded very complicated and not clean (and I started being stuck anyway, as making it available for one hour or several hours or several days makes it even more complicated)
Is there a gem that could do this for me? If I have to do it by hand, what's the best way to begin?
Thanks a lot
Just create an instance method available? which tests there are no bookings that overlap the from to range. You can use none? on the relationship.
class Space
has_many :bookings
def available?(from, to)
bookings.where('start_booking <= ? AND end_booking >= ?', to, from).none?
end
end
Taking some inspiration from the answer of SteveTurczyn. The following might give you some inspiration.
class Space < ApplicationRecord
# attributes: id
has_many :bookings
def self.available(period)
bookings = Booking.overlap(period)
where.not(id: bookings.select(:space_id))
end
def available?(period)
if bookings.loaded?
bookings.none? { |booking| booking.overlap?(period) }
else
bookings.overlap(period).none?
end
end
end
class Booking < ApplicationRecord
# attributes: id, space_id, start, end
belongs_to :space
def self.overlap(period)
period = FormatConverters.to_period(period)
# lteq = less than or equal to, gteq = greater than or equal to
# Other methods available on attributes can be found here:
# https://www.rubydoc.info/gems/arel/Arel/Attributes/Attribute
where(arel_table[:start].lteq(period.end).and(arel_table[:end].gteq(period.start)))
end
def overlap?(period)
period = FormatConverters.to_period(period)
self.start <= period.end && self.end >= period.start
end
module FormatConverters
module_function
def to_period(obj)
return obj if obj.respond_to?(:start) && obj.respond_to?(:end)
obj..obj
end
end
end
With the above implemented you can query a single space if it is available during a period:
from = Time.new(2019, 10, 1, 9, 30)
to = Time.new(2019, 10, 5, 17, 30)
period = from..to
space.available?(period) # true/false
You can get all spaces available:
spaces = Space.available(period) # all available spaces during the period
Note that class methods will also be available on the scope chain:
spaces = Space.scope_01.scope_02.available(period)
I've also added the overlap scope and overlap? helper to simplify creating the above helpers.
Since in my version Booking has a start and end attribute (similar to Range) you can also provide it to any methods accepting a period.
booking_01.overlap?(booking_02) # true/false
To retrieve all bookings that that overlap this very moment:
bookings = Booking.overlap(Time.now) # all bookings overlapping the period
Hope this gave you some inspiration. If you'd like to know how the overlap checking works I have to forward you to this question.
Note: This answer assumes that the provided period is valid. A.k.a. start <= end. If you for some reason provide Time.new(2019, 10, 1)..Time.new(2019, 9, 23) the results are going to be skewed.
I've got a background job that I run about 5,000 of them every 10 minutes. Each job makes a request to an external API and then either adds new or updates existing records in my database. Each API request returns around 100 items, so every 10 minutes I am making 50,000 CREATE or UPDATE sql queries.
The way I handle this now is, each API item returned has a unique ID. I search my database for a post that has this id, and if it exists, it updates the model. If it doesn't exist, it creates a new one.
Imagine the api response looks like this:
[
{
external_id: '123',
text: 'blah blah',
count: 450
},
{
external_id: 'abc',
text: 'something else',
count: 393
}
]
which is set to the variable collection
Then I run this code in my parent model:
class ParentModel < ApplicationRecord
def update
collection.each do |attrs|
child = ChildModel.find_or_initialize_by(external_id: attrs[:external_id], parent_model_id: self.id)
child.assign_attributes attrs
child.save if child.changed?
end
end
end
Each of these individual calls is extremely quick, but when I am doing 50,000 in a short period of time it really adds up and can slow things down.
I'm wondering if there's a more efficient way I can handle this, I was thinking of doing something instead like:
class ParentModel < ApplicationRecord
def update
eager_loaded_children = ChildModel.where(parent_model_id: self.id).limit(100)
collection.each do |attrs|
cached_child = eager_loaded_children.select {|child| child.external_id == attrs[:external_id] }.first
if cached_child
cached_child.update_attributes attrs
else
ChildModel.create attrs
end
end
end
end
Essentially I would be saving the lookups and instead doing a bigger query up front (this is also quite fast) but making a tradeoff in memory. But this doesn't seem like it would be that much time, maybe slightly speeding up the lookup part, but I'd still have to do 100 updates and creates.
Is there some kind of way I can do batch updates that I'm not thinking of? Anything else obvious that could make this go faster, or reduce the amount of queries I am doing?
You can do something like this:
collection2 = collection.map { |c| [c[:external_id], c.except(:external_id)]}.to_h
def update
ChildModel.where(external_id: collection2.keys).each |cm| do
ext_id = cm.external_id
cm.assign_attributes collection2[ext_id]
cm.save if cm.changed?
collection2.delete(ext_id)
end
if collection2.present?
new_ids = collection2.keys
new = collection.select { |c| new_ids.include? c[:external_id] }
ChildModel.create(new)
end
end
Better because
fetches all required records all at once
creates all new records at once
You can use update_columns if you don't need callbacks/validations
Only drawback, more ruby code manipulation which I think is a good tradeoff for db queries..
I'm creating an application that creates polls, each poll has many poll pages, and each poll page has many question clusters, what I want to do is that when a question cluster is deleted, search every question clusters from the same page that had a higher position, and diminish 1.
This is what I tried, but it doesn't even runs:
after_destroy :reassign_position
private
def reassign_position
question_clusters = QuestionCluster.where(poll_page_id: self.poll_page_id)
question_clusters.where("position > ?", self.position)
quest_cluster.each do |question_cluster|
question_cluster.position -= 1
end
end
How can I accomplish what I want?
You are not updating the question_cluster's attribute (position). Take a look:
def reassign_position
question_clusters = QuestionCluster.where(poll_page_id: self.poll_page_id)
question_clusters.where("position > ?", self.position)
quest_cluster.each do |question_cluster|
# actually update the question_cluster
question_cluster.update!(position: question_cluster.position - 1) # <========
end
end
I'm building a rails 3 app in which it sells a limited number of items. I'm looking for a way to hold an item for a specific amount of time so that when someone selects an item, they have time to purchase it before someone else can purchase it before them. I have done some research as to row locking but haven't found a usable method thus far for specifying a time.
Thanks for any help or ideas
This is a typical workflow pattern, where you acquire an object for a duration. You can achieve this easily by implementing application level locks.
1) Add lock fields to the model.
locker_id
lock_until
2) Now you can implement this logic in the Product model.
class Product
belongs_to :locker, :class_name => "User",
:condition => lambda { {:conditions => ["lock_until < ? ", Time.now]}}
def locked?
!lock_until.nil? and lock_until > Time.now
end
def lock_for_duration(usr, duration=10.minutes)
return false if locked?
self.locker_id = user.id
self.lock_until = duration.from_now
self.save
end
def release_lock
return true unless locked?
self.locker_id = nil
self.lock_until = nil
self.save
end
end
Here is how to use this:
usr = User.first
product.lock_for_duration(usr, 30.minutes)
product.locked?
product.locker?
I would recommend setting a locked_until timestamp that's checked whenever someone attempts to buy one of these items. If there are no items with a locked_until time in the past, then all items are "sold out". For actual selling of the items, I would have a sold boolean field.