Django ORM multiple distinct and order by - python

I work on a simple chat app. I need to query db for an user to get last message of user conversation to other users. Same as main page of whatsapp and telegram.
Model:
class CHAT(models.Model):
sender_uid = models.CharField(max_length=150, db_index=True)
receiver_uid = models.CharField(max_length=150, db_index=True)
message = models.TextField(verbose_name='Message Text')
created = models.IntegerField(default=created_time_epoch)
I tried this query:
message_list = self.model.objects.filter(Q(sender_uid=user_uid)|Q(receiver_uid=user_uid)).order_by('receiver_uid', 'sender_uid', '-created').distinct('receiver_uid', 'sender_uid')
Output:
<QuerySet [<CHAT: ted#ted.com Message: hello 4 To: saeed#saeed.com>, <CHAT: marshal#marshal.com Message: hello6 To: saeed#saeed.com>, <CHAT: saeed#saeed.com Message: hello 5 To: ted#ted.com>]>
My problem is I get two last message from each conversation (if both user send message to each other), In one of them user is sender and in other one user is receiver.
For now I handle it with below code:
message_send_list = list(self.model.objects.filter(sender_uid=user_uid).order_by('receiver_uid', '-created').distinct('receiver_uid'))
message_receive_list = list(self.model.objects.filter(receiver_uid=user_uid).order_by('sender_uid', '-created').distinct('sender_uid'))
temp_list = []
for s_message in message_send_list:
r_message = next((item for item in message_receive_list if item.sender_uid == s_message.receiver_uid), None)
if r_message is not None:
message_receive_list.pop(message_receive_list.index(r_message))
if s_message.created > r_message.created:
temp_list.append(s_message)
else:
temp_list.append(r_message)
else:
temp_list.append(s_message)
temp_list.extend(message_receive_list)
Output:
[<CHAT: saeed#saeed.com Message: hello 5 To: ted#ted.com>, <CHAT: marshal#marshal.com Message: hello6 To: saeed#saeed.com>]
My question is how can I get this result in one query? Problem is user can be sender and receiver of message and I can't distinguish which one is last message of conversation. How to filter or distinct on that?

Based on the description of the problem, you make it a bit too complex. You can obtain the other person with a conditional expression [Django-doc]. So by first making a "reduction" where we take the other person, we can then use a uniqueness filter for that:
from django.db.models import Case, F, When
last_messages = self.model.objects.filter(
Q(sender_uid=user_uid) | Q(receiver_uid=user_uid)
).annotate(
other=Case(
When(sender_uid=user_uid, then=F('receiver_uid')),
default=F('sender_uid'),
output_field=CharField()
)
).order_by('other', '-created').distinct('other')
Furthermor all Chat objects will have an extra attribute: other that thus contains the non-user_uid side.

Related

How do you add one object to a Django Model that has a ManyToManyField?

I am trying to create a social media type site that will allow a user to follow and unfollow another user. followers has a ManyToManyField because a user can have many followers.
models.py
class Follower(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, default="")
followers = models.ManyToManyField(User, related_name="followers")
views.py
def username(request, user):
#get user
user = get_object_or_404(User.objects, username=user)
posts = Post.objects.filter(user=user).order_by('-date_and_time')
#follow button code
follow_or_unfollow = ''
try:
following = get_object_or_404(Follower, Q(
user=user) & Q(followers=request.user))
print(following)
except:
following = False
if following:
follow_or_unfollow = True
else:
follow_or_unfollow = False
if request.POST.get('follow'):
follower = Follower.objects.create(user=request.user)
follower.followers.add(*user)
follow_or_unfollow = False
elif request.POST.get('unfollow'):
follow_or_unfollow = True
#following.delete()
When it gets the 'follow' POST request, I want it to add the user who sent it (the one that is logged in) to be added to the followers. Right now, I am getting this error when I try to do that.
TypeError: django.db.models.fields.related_descriptors.create_forward_many_to_many_manager.<locals>.ManyRelatedManager.add() argument after * must be an iterable, not User
I know it says that it has to be iterable, but is there any way to just add one object at a time. Also, how would you delete this particular object?
The * in the arguments converts a list to individual args. For example-
lst = [1,2,3,4,5]
function(*lst)
can be just read as
function(1,2,3,4,5)
You have used follower.followers.add(*user). Hence, user must be an iterable to be unpacked and passed as a list or arguments. But user is a single User object.
You should just use follower.followers.add(user) in this case.

how to use backref in peewee

basically im trying to write a index route that returns the posts of a business that a user is subscribed to
the last line is throwing an error for the backref (business.posts)
# query that finds all the subscriptions of the logged in user
subscriptions_query = models.Subscription.select().join(models.User).where(models.User.id == current_user.id)
# gets the businesses being followed from the subscriptions
businesses_being_followed = [subscription.following for subscription in subscriptions_query]
post_dicts = [model_to_dict(business.posts) for business in businesses_being_followed]
this is my posts model
class Post(BaseModel):
business = ForeignKeyField(Business, backref='posts')
image = CharField()
content = CharField()
date = DateTimeField(default=datetime.datetime.now)
Your example is REALLY inefficient.
Can you just do:
(Post
.select()
.join(Business)
.join(Subscription)
.where(Subscription.user == the_user_id))

Django ORM: Object is not iterable (Error)

Can anyone explain why this is iterable:
User.objects.all()
this is valid and gives me a value (The current user's alias. session is storing the user id):
User.objects.get(id = request.session['currentuser']).alias)
But this is giving me the error saying it is 'not iterable?':
Poke.objects.get(user = User.objects.get(id = request.session['currentuser']).alias)
(This code is supposed to get a list of Poke entries where the user column matches the current user's alias.)
Here is the Poke model. It does not use ForeignKeys, as I was having trouble setting two of them without errors.
class Poke(models.Model):
id = models.IntegerField(primary_key=True)
user = models.CharField(max_length=100)
poker = models.CharField(max_length=100)
pokes = models.IntegerField()
class Meta:
app_label = "poke_app"
Get will retrieve a single object and therefore the result will not be iterable. See documentation.
Do you see an integer value when you print(request.session['currentuser'])?
If you will see a string then you shoud give an integer value
EX: userobj = User.objects.get(id=uid)
Oh sory
User.objects.get(id = request.session['currentuser']).alias)
You open ( and closed it after ['currentuser']) but why you close ) again after .alias ?

How to show the last message of each user to user conversations to keep a chat history?

I'm creating a private user to user chat, in order to chat with someone the connected user has to type the username of the user with whom he wants to talk to on his own url.
Now that this system is already built, I want to keep a chat history so that later on I can send notification of chat. To do that I need to get the last message of each conversations and I want to show it on the connected user's own chat profile.
Just as the image below :
Model userComment fields are : recipient, sender, comment, sent_at
views.py :
def inbox(request, username):
username = User.objects.get(username=username)
connected_user = request.user
if username == connected_user:
#I'm having the issue on this line
users = userComment.objects.filter(Q(client=request.user) | Q(worker=request.user)).order_by(?)
else:
users = userComment.objects.filter(Q(Q(client=request.user) & Q(worker=username)) | Q(Q(client=username) & Q(worker=request.user))).order_by('sent_at')
models.py
class userComment(models.Model):
client = models.ForeignKey(User, related_name="client")
worker = models.ForeignKey(User, blank=True, null=True, related_name="worker")
sent_at = models.DateTimeField(auto_now_add=True)
comment = models.TextField(max_length=255, null=True)
def __str__(self):
return str(self.client)
Question : How can I filter and order my view to do so ?
Firstly in your userComment model add a related query name for reverse relation
class UserComment(models.Model):
sender = models.ForeignKey(User, related_name='sender', related_query_name='s')
recipient = models.ForeignKey(User, related_name='recipient', related_query_name='r')
sent_at = models.DateTimeField(auto_now_add=True)
comment = models.TextField()
Now in your views.py use this query:
user = request.user
users = User.objects.filter(Q(r__sender=user) | Q(s__recipient=user)).distinct().extra(select={'last_message_time': 'select MAX(sent_at) from appname_usercomment where (recipient_id=auth_user.id and sender_id=%s) or (recipient_id=%s and sender_id=auth_user.id)'}, select_params=(user.id, user.id,)).extra(order_by=['-last_message_time']).extra(select={'message': 'select comment from appname_usercomment where (sent_at=(select MAX(sent_at) from appname_usercomment where (recipient_id=auth_user.id and sender_id=%s) or (recipient_id=%s and sender_id=auth_user.id)) and ((recipient_id=auth_user.id and sender_id=%s) or (recipient_id=%s and sender_id=auth_user.id)))',}, select_params=(user.id, user.id,user.id, user.id,))
Set the appname in extra according to name of the app in which the model is.
Now you can access it as follows:
for user in users:
print user.username
print user.last_message_time
print user.message
def inbox(request, username)
# first select all the comments related to user
user = User.objects.get(username=username)
related = userComment.objects.filter(q(client=user) | q(worker=user)).order_by('-sent_at')
# This selects the latest comments.
# Now loop over the related comments and group them.
chats = {}
for comment in related:
if comment.client == user:
previous_chat_history = chats.setdefault(comment.worker.username, [])
if not len(previous_chat_history) >= 3:
previous_chat_history.append(comment)
if comment.worker== user:
previous_chat_history = chats.setdefault(comment.client.username, [])
if not len(previous_chat_history) >= 3:
previous_chat_history.append(comment)
# Reverse each list to keep the latest message last
for k, v in chats.items():
chats[k] = v.reverse()
return render(request, 'template.html', context={chats: chats})
[Update]: I just realized that this solution will only work with Postgresql because it is using field names in distinct.
You can mix order_by and distinct to achieve the desired results:
filter the comments where the user is either client or a worker:
comments = userComment.objects.filter(Q(client=request.user) | Q(worker=request.user))
order the user comments with client, worker and sent_at fields. Make sure to have a descending order for sent_at field so the latest comments for each client-worker pair are at top:
comments = comments.order_by('client', 'worker', '-sent_at')
Now, get the distinct rows:
comments = comments.distinct('client', 'worker')
This will keep only the first row which is the latest comment for each for each client-worker pair and delete the rest of the rows from the queryset.
In one statement:
comments = userComment.objects \
.filter(Q(client=request.user) | Q(worker=request.user)) \
.order_by('client', 'worker', '-sent_at') \
.distinct('client', 'worker')
This will give you the latest comment for each conversation where the user is either a client or a worker.

Django ManyToMany filtering by set size or member in the set

I am using django to maintain a database of messages.
Among others I have the following models:
class User(models.Model):
id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=10)
class Message(models.Model):
id = models.IntegerField(primary_key=True)
body = models.CharField(max_length=200)
users = models.ManyToManyField(User)
I am trying to write a utility method that for a given user gives me the messages he (and he alone) is associated with.
i.e. for:
m1 = Message(id=1, body='Some body')
m1.save()
m2 = Message(id=2, body='Another body')
m2.save()
m3 = Message(id=3, body='And yet another body')
m3.save()
u1 = User(name='Jesse James')
u1.save()
u2 = User(name='John Doe')
u2.save()
m1.users.add(u1, u2)
m2.users.add(u1)
m3.users.add(u2)
getMessagesFor('Jesse James')
Will return only m2.
Assuming I have in user the right model instance, it boils down to one line, and I have tried these following:
user.message_set.annotate(usr_cnt=Count('users')).filter(usr_cnt__lte=1)
Or:
messages = Message.objects.filter(users__id__in=[user.id])
And:
messages = Message.objects.filter(users__id__exact=user.id)
And:
messages = Message.objects.filter(users__contains=user)
And so on... I always get both m2 AND m1.
Tried annotations, excludes, filters etc.
Can someone help me with this?
qs = Message.objects.annotate(cc=Count('users')).filter(cc=1)
Above query will return all messages which has only single user associated with it.
To filter by user, add another filter at end to filter the annotated query according to user:
qs = Message.objects.annotate(cc=Count('users')).filter(cc=1).filter(users__id=user.id)
# if user user.id=1, this will return only m2
Something like this maybe? (not tested)
for msg in Messages.objects.all():
if (user in msg.users_set.all() and len(msg.users_set.all()) == 1):
# do something

Categories

Resources