How do I check whether or not each row in a table is equivalent to a value in SQL Alchemy and return a corresponding Boolean? - join

Problem
I am trying to make a reddit style voting application that allows users to vote on various posts in a database. I don't want a user to be able to vote on a post more than once. As such, I want to return a query with the post data that also displays a boolean value to indicate whether or not the currently logged in user has voted on each post.
What I'm looking for is the ability to query the database and if the currently logged in user has already voted on post (1, 2, and 4 out of 5 total posts) then the query should return this
{post: {...data...}, voted: true}
{post: {...data...}, voted: true}
{post: {...data...}, voted: false}
{post: {...data...}, voted: true}
{post: {...data...}, voted: false}
What I've Tried
use exists().where() to return a boolean if the user_id in the votes table is equal to the current users id.
def get_posts(db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user)
posts = db.query(
models.Post,
exists().where(models.Vote.user_id == current_user.id).label("voted")
).join(
models.Vote, models.Vote.post_id == models.Post.id, isouter=True
).group_by(
models.Post.id
).all()
This one seems to be no good because if a user has voted on any single post in the database then the query will return True for every post when that user is logged in. It returns this.
{post: {...data...}, voted: true}
{post: {...data...}, voted: true}
{post: {...data...}, voted: true}
{post: {...data...}, voted: true}
{post: {...data...}, voted: true}
I have also tried implementing a count to determine the number of times that the current user has voted on a post. My thinking was that if a 1 is returned that can be treated as representing True whereas if a 0 is returned that can be treated as representing False. This however seems to be no good as it keeps a count of all the votes from all users in the database. As such if more than one user votes on a post it will have a value of 2, 3, 4 etc... which does not help me determine whether or not the current user has voted on the post or not.
Here is the code for this query
def get_posts(db: Session = Depends(get_db), current_user: int = Depends(oauth2.get_current_user)
posts = db.query(
models.Post,
func.count(models.Vote.user_id == current_user.id).label("voted")
).join(
models.Vote, models.Vote.post_id == models.Post.id, isouter=True
).group_by(
models.Post.id
).all()
For reference these are my models
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql.expression import text
from sqlalchemy.sql.sqltypes import TIMESTAMP
from .database import Base
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, nullable=False)
title = Column(String, nullable=False)
content = Column(String, nullable=False)
published = Column(Boolean, server_default="True", nullable=False)
created_at = Column(TIMESTAMP(timezone=True), server_default=text('now()'), nullable=False)
owner_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
owner = relationship("User", backref="posts")
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, nullable=False)
username = Column(String, nullable=False, unique=True)
email = Column(String, nullable=False, unique=True)
password = Column(String, nullable=False)
created_at = Column(TIMESTAMP(timezone=True), server_default=text('now()'), nullable=False)
class Vote(Base):
__tablename__ = "votes"
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True)
user = relationship("User", backref="votes")
post_id = Column(Integer, ForeignKey("posts.id", ondelete="CASCADE"), primary_key=True)
post = relationship("Post", backref="votes")
upvote = Column(Boolean, nullable=False)
Thank you for taking the time to read this! Any help or documentation of specific methods/solutions you can point me towards would be greatly appreciated!

Using LEFT OUTER JOIN conditioned on the current user id. This works I think but usually breaks down quickly if you need to start doing other joins and is error prone. The subquery solution is better.
q = session.query(
Post,
(Vote.post_id != None).label('user_voted'),
).join(
Vote, and_(Post.id == Vote.post_id, Vote.user_id == current_user_id), isouter=True
).group_by(
Post.id, Vote.post_id, Vote.user_id
)
SELECT posts.id AS posts_id, posts.title AS posts_title, votes.post_id IS NOT NULL AS user_voted
FROM posts LEFT OUTER JOIN votes ON posts.id = votes.post_id AND votes.user_id = %(user_id_1)s GROUP BY posts.id, votes.post_id, votes.user_id
Using sqlalchemy 1.4/2.0 new select() statement to generate subquery:
subq = select(Vote.post_id).distinct().where(Vote.user_id == current_user_id)
q = session.query(Post, Post.id.in_(subq).label('user_voted'))
SELECT posts.id AS posts_id, posts.title AS posts_title, posts.id IN (SELECT DISTINCT votes.post_id
FROM votes
WHERE votes.user_id = %(user_id_1)s) AS user_voted
FROM posts

Related

Rails: Conditional subquery MAX(effective_date)

Let's suppose, there are three tables in the database:
courses (:id, :name)
course_details (:course_id, :effective_date, :status),
course_codes (:course_detail_id, :code)
course has many course_details
course_detail has many cource_codes
Course can have multiple course_details records which are effective_dated applicable (means only one record of course_detail will be used in the system).
Problem statement: I want to filter courses by course codes given code. And course should only be filtered by the course_codes which are linked with effective dated course_detail and should skip the past effective dated records.
course = Course.find(params[:id])
course_detail = CourseDetail.find_by(effective_date: CourseDetail.max(effective_date), course_id: course.id)
If I use this code this will filter course irrespective of effective_dated course_details:
Course.left_joins(course_details: :course_codes).where(course_details: { course_codes: { code: params[:code] } })
courses:
Id
Name
1
English
2
Maths
course_details:
id
course_id
effective_date
1
1
2020-10-01
2
1
2021-01-01
3
2
2020-09-01
course_codes:
id
course_detail_id
code.
1
1
eng-01
2
2
eng-505
3
3
math-01
when I pass code = eng-01 it should return empty array instead of course with id 1.
Can somebody please help me?
To resolve this issue, I used a subquery that returns ids of course_details of all the courses according to effective_date:
query = "select child.id from courses as parent
inner join course_details as child on child.course_id = parent.id
where child.effective_date =
(select max(child1.effective_date) as effective_date
from course_details as child1
where child1.course_id = parent.id
and (child1.effective_date <= CURRENT_DATE
or child1.effective_date = (select min(child2.effective_date) as effective_date
from course_details as child2
where child2.course_id = parent.id)
))"
effective_dated_ids = Course.find_by_sql(query).pluck(:id)
After getting all the ids, I passed these ids in search.
records = Course.left_joins(course_details: :course_codes).where(course_details: { id: effective_record_ids, course_codes: { course_code: params[:course_code] } })
And it worked as expected.

Scope Order by Count with Conditions Rails

I have a model Category that has_many Pendencies. I would like to create a scope that order the categories by the amount of Pendencies that has active = true without excluding active = false.
What I have so far is:
scope :order_by_pendencies, -> { left_joins(:pendencies).group(:id).order('COUNT(pendencies.id) DESC')}
This will order it by number of pendencies, but I want to order by pendencies that has active = true.
Another try was:
scope :order_by_pendencies, -> { left_joins(:pendencies).group(:id).where('pendencies.active = ?', true).order('COUNT(pendencies.id) DESC')}
This will order by number of pendencies that has pendencies.active = true, but will exclude the pendencies.active = false.
Thank you for your help.
I guess you want to sort by the amount of active pendencies without ignoring categories that have no active pendencies.
That would be something like:
scope :order_by_pendencies, -> {
active_count_q = Pendency.
group(:category_id).
where(active: true).
select(:category_id, "COUNT(*) AS count")
joins("LEFT JOIN (#{active_count_q.to_sql}) AS ac ON ac.category_id = id").
order("ac.count DESC")
}
The equivalent SQL query:
SELECT *, ac.count
FROM categories
LEFT JOIN (
SELECT category_id, COUNT(*) AS count
FROM pendencies
GROUP BY category_id
WHERE active = true
) AS ac ON ac.category_id = id
ORDER BY ac.count DESC
Note that if there are no active pendencies for a category, the count will be null and will be added to the end of the list.
A similar subquery could be added to sort additionally by the total amount of pendencies...
C# answer as requested:
method() {
....OrderBy((category) => category.Count(pendencies.Where((pendency) => pendency.Active))
}
Or in straight SQL:
SELECT category.id, ..., ActivePendnecies
FROM (SELECT category.id, ..., count(pendency) ActivePendnecies
FROM category
LEFT JOIN pendency ON category.id = pendency.id AND pendnecy.Active = 1
GROUP BY category.id, ...) P
ORDER BY ActivePendnecies;
We have to output ActivePendnecies in SQL even if the code will throw it out because otherwise the optimizer is within its rights to throw out the ORDER BY.
For now I developed the following (it's working, but I believe that it's not the best way):
scope :order_by_pendencies, -> { scoped = Category.left_joins(:pendencies)
.group(:id)
.order('COUNT(pendencies.id) DESC')
.where('pendencies.active = ?', true)
all = Category.all
(scoped + all).uniq}

Rails: How to access session parameter / ActiveRecord::StatementInvalid in Orders#create

I am working on a multistep form for an order placement section which uses a session session[:order_params] to store all form inputs before submit.
I need to be able to access a particular parameter (land) from the session in order to query for another resource (shippingservice) when navigating back in the form.
In my orders_controller.rb I have:
#shippingservices = #cart.available_shipping_services.joins(:lands).where(:lands => {:id => params[:id]})
but would need to specify the land.id from the session[:order_params].
When using session[:order_params] I get ActiveRecord::StatementInvalid in Orders#create:
Mysql::Error: Unknown column 'id.ship_to_last_name' in 'where clause': SELECT `shippingservices`.* FROM `shippingservices`
INNER JOIN `zones` ON `zones`.`id` = `shippingservices`.`zone_id`
INNER JOIN `lands_zones` ON `lands_zones`.`zone_id` = `zones`.`id`
INNER JOIN `lands` ON `lands`.`id` = `lands_zones`.`land_id`
WHERE `id`.`ship_to_last_name` = 'Smith'
AND `id`.`ship_to_address` = 'Somewherestreet'
AND `id`.`ship_to_city` = 'Nowheretown'
AND `id`.`ship_to_postal_code` = '99999'
AND `id`.`phone_number` = 'some number'
AND `id`.`shippingservice_id` = '34'
AND `id`.`email` = 'someone#example.tld'
AND `id`.`land_id` = '85'
AND `id`.`ship_to_first_name` = 'John'
AND (weightmin <= 200 AND weightmax >= 200 AND heightmin <= 12 AND heightmax >= 12 AND shippingservices.shippingcarrier = '1') AND (lengthmax >= 210 AND widthmax >= 149)
Since the correct land_id is present I am wondering how to provide only that value to the query.
Thank you in advance!
As per the description mentioned in the post, you want to access a particular key stored in session at a particular key.
Assuming order_params is a hash, you can get land_id using the below mentioned code:
session[:order_params][:land_id]
This will return the value of land_id and thus you can use it in the query.
To set session variable, you can set some data in a controller action
For eg:
app/controllers/sessions_controller.rb
def create
# ...
session[:current_user_id] = #user.id
# ...
end
And read it in another: app/controllers/users_controller.rb
def index
current_user = User.find_by_id(session[:current_user_id])
# ...
end

Combine two ActiveRecord results and sort by a shared joined table attribute

I have a Convo table and a GroupMeeting table that both are associated with a Msg table.
I want to find all the instances where the current_user has convos or group_meetings with msgs, combine the two, and then show both together to the user in order of the last msg.created_at
Here I have defined both:
#convos = Convo.includes(:msgs).where("sender_id = ? OR recipient_id = ?", current_user, current_user).where.not(:msgs => { :id => nil }).merge(Msg.order(created_at: :desc))
#group_meetings = current_user.group_meetings.includes(:msgs).where.not(:msgs => { :id => nil }).merge(Msg.order(created_at: :desc))
And then combined them together:
#convos = #convos + #group_meetings
What I can't figure out is how to now sort them by msg.created_at
I have tried the following:
#convos = (#convos + #group_meetings).sort_by(&:"#{msg.created_at}")
#convos.order('msg.created_at DESC')
These all seem to be server-side sorting though. How can I sort these based off the join table, after the array has been created?
Please let me know if I need to supply any other details. Thank you!!
You can try the following:
(#convos + #group_meetings).sort_by { |item| item.msgs.minimum(:created_at) }

Group by in Rails View

In my rails application I have following models
Transaction
belongs_to :account
belongs_to :agent
belongs_to :program
And here is the query i used to get data
def self.efficiency_report(starts=nil, ends=nil)
sql = "SELECT p.abbreviation,ag.name,
t.miles, t.date
FROM transactions t
inner join accounts a on t.account_id = a.id
inner join programs p on a.program_id = p.id
inner join agents ag on t.agent_id = ag.id
Group by p.id , ag.id"
result_array(sql)
end
def self.result_array(sql)
conn = ActiveRecord::Base.connection
res = conn.execute(sql)
results = []
res.each{|r|
results << r
}
return results
end
I want to render data in view with group by program first then group by agent name under it then miles, like this
Program:AA
Agent:Bob
Miles Date
1234 02/12/2012
5463 03/12/2012
Agent:Ben
Miles Date
234 02/22/2012
344 01/02/2012
Program:BB
Agent:Bob
Miles Date
1234 02/12/2012
5463 03/12/2012
Agent:Ben
Miles Date
234 02/22/2012
344 01/02/2012
For this i m doing following in my view
%h2 Vendor Efficiency Report
- #transactions.group_by(&:row[0]).sort.each { |data, transaction|
%h2= data.row[0]
- transaction.group_by(&:row[1]).sort.each { |data, transaction|
%table#data_table.display{:cellpadding => "0", :cellspacing => "0"}
%h3{:style => "clear:both;margin-left:10px"}= data.row[1]
%thead
%tr.odd
%td.bold{:style => "width:60px;"} Miles
%td.bold{:style => "width:60px;"} Date
- for t in transaction
%tbody
%tr
%td{:style => "width:60px;"}= row[2]
%td{:style => "width:60px;"}= row[3]
-}
-}
= will_paginate #transactions
But i m getting this error
wrong argument type String (expected Proc)
Will anyone let me know what i m doing wrong here or is there any other better way for grouping?
Thanks in advance
The problem is your group_by calls. I'm not sure what &:row[0] is doing, but it's passing a string as argument. I think this is what you're looking for:
#transactions.group_by{ |r| r[0] }...
Edit
Just figured out what &:row[0] was doing. It's a call to the Symbol#[] method, which returns the character at the index given (essentially, as if it were aString). The call :row[0] returns the first character of the string "row".
So basically, it was as if you were calling:
#transactions.group_by(&"r")

Resources