I'm trying to build a query which returns all objects which have children matching specified criteria. The trick is that there are multiple criteria which are mutually exclusive, so there must be multiple children. I'm not sure how to express this.
Model:
class Message(Base):
__tablename__ = 'Message'
class MessageRecipient(Base):
__tablename__ = 'MessageRecipient'
recipient_id = Column(Integer, ForeignKey('User.uid'))
message_id = Column(Integer, ForeignKey('Message.uid'))
user = relationship('User', backref="messages_received")
message = relationship('Message', backref="recipients")
I want to get all messages which are being sent to a defined set of users. For example, I want to return all messages which were sent to users 1 and 2, but not messages only sent to user 1 or messages only sent to user 2. It must have been sent to both users!
I was trying a query like the following:
query = Message.query.filter(Message.recipients.any(MessageRecipient.recipient_id.in_([1,2])))
The above doesn't work because in_ is disjunctive. It does return the messages I want, but it also returns messages I don't want.
Does anyone have an idea of how I can build a query which requires that a Message have MessageRecipients with an arbitrary set of ids?
I solved this by iterating over all objects in the collection and creating a new subquery for each using exists(). Not sure if this is the most efficient way, but it works.
for recipient in [1,2]:
query = query.filter(MessageRecipient.query.filter(and_(MessageRecipient.recipient_id== recipient,
MessageRecipient.message_id == Message.uid)).exists())
Related
I'm currently working on a practice social media app. In this app, current users can invite their friends by email to join the app (specifically, joining a 'channel' of the app, like Discord). For this project, I'm working on functionality where a user will get an error if they try to invite someone who is already in the app (meaning people who are already in the app's database). I'm working on unit tests that ensures the error messages pop up when users are detected as already existing.
I managed to get my first scenario working, but I'm a bit stumped for the second one.
Here is a file that is central to both tests.
factories.py
class ChannelFactory(factory.django.DjangoModelFactory)
class Meta:
model = Channel
id = int
name = str
class CurrentUserFactory(factory.django.DjangoModelFactory)
class Meta:
model = CurrentUser
user_email = user_email
channel = models.ForeignKey(Channel)
Scenario #1 (currently working) - one new user is invited to join the app but already exists in the app's database
test_forms.py
from tests.factories import ChannelFactory, CurrentUserFactory
#pytest.mark.django_db
def test_that_current_user_cannot_be_reinvited(email, error_message):
"""user that is already in the specific channel cannot be reinvited"""
email = "user#test.com"
error_message = "user#test.com already exists in this channel"
# I am not specifying the channel name because the factory object is supposed to generate it automatically
current_user = CurrentUserFactory(user_email='user#test.com')
invite_form = forms.UserRequestForm({"email":email, channel=current_user.channel)
assert not invite_form is valid()
assert invite_form.errors["email"][0] = error_message
Result: Test passes!
However, the test passes mainly because there's only one user being tested.
So now, my task is to create a test to see what happens if several people are invited at once. The app allows for a comma separated string to be entered, so theoretically up to ten emails can be invited at a time.
Scenario #2 - two new users are invited to the same channel and both exist in the app's database. Here's where I'm running into issues because I need to somehow make sure the CurrentUsers are generated into the same channel.
from tests.factories import CurrentUserFactory
#pytest.mark.django_db
def tests_that_multiple_current_users_cannot_be_reinvited(emails, error_message):
"""users that are already in the specific channel cannot be reinvited"""
emails = ["user#test.com", "user2#test.com"]
error_message = "The following users already exist in this channel: user#test.com, user2#test.com"
#here, I attempt to "force" a Channel instance
channel = Channel(id="5", name="Hometown Friends")
current_users = [
(CurrentUserFactory(user_email='user#test.com', channel=channel)),
(CurrentUserFactory(user_email='user2#test.com', channel=channel)),
]
invite_form = forms.UserRequestForm({"email":emails, "channel":current_users.channel})
assert not invite_form is valid()
assert invite_form.errors["email"][0] = error_message
When I try running the test:
E AttributeError: 'tuple' object has no attribute 'channel'
I'm wondering if I can easily solve this by trying to work with the tuple, or if there's a much easier method that I'm somehow not seeing. Help would be greatly appreciated!
I've got the following models...
class User(ndb.Model):
email = ndb.StringProperty()
username = ndb.StringProperty(indexed=True)
password = ndb.StringProperty()
class Rel(ndb.Model):
user = ndb.KeyProperty(kind=User, indexed=True)
follows = ndb.KeyProperty(kind=User, indexed=True)
blocks = ndb.KeyProperty(kind=User)
I'm trying to make it so a user can follow or block any other number of users.
Using the above setup I'm finding it hard to perform tasks that would been easy with a traditional DBMS.
As a simple example, how would I find all of a given user's followers AND order by username-- keeping in mind when I perform a query on Rel, I'm getting back keys and not user objects?
Am I going about this the wrong way?
You have to do a fetch but you can go about designing it in a better way,
the follows and blocks fields can be lists instead of just key -
follows = ndb.KeyProperty(kind=User, repeated=True)
blocks = ndb.KeyProperty(kind=User, repeated=True)
after this when you need the follows of this user you can get the keys and do an ndb.get_multi(Keys_list) to get all the follows/blocks entities whatever you need.
OR
A better way of doing this -
If you care about the order and want to paginate, you will have to store all the follow/block entities separately,
for example if this is about a user 'a'
Follows entity will have records for each person 'a' follows
class FollowEntity(ndb.Model):
user = ndb.KeyProperty(kind=User)
follow = ndb.KeyProperty(kind=User)
follow_username = ndb.StringProperty()
a query can be
assuming user is an entry from your 'User' Entity.
query = FollowEntity.query(FollowEntity.user == user.key).order(FollowEntity.follow_username)
you can run this query and get the sorted username results, would work well if you use fetch_page to display the results in a batch.
Do the same for BlockEntity too
I have two models: User and Message. Each Message has two references to User (as sender and as receiver). Also I have defined an other_user hybrid method for Message which returns "user other than specific one" - see below:
from peewee import *
from playhouse.hybrid import hybrid_method
from playhouse.shortcuts import case
class User(Model):
name = CharField()
class Message(Model):
sender = ForeignKeyField(User, related_name='messages_sent')
receiver = ForeignKeyField(User, related_name='messages_received')
text = TextField()
#hybrid_method
def other_user(self, user):
if user == self.sender:
return self.receiver
elif user == self.receiver:
return self.sender
else:
raise ValueError
#other_user.expression
def other_user(cls, user):
return case(user.id, (
(cls.sender, cls.receiver),
(cls.receiver, cls.sender)))
Now I want to make a composite query which will retrieve all messages for current user and also retrieve information about "other" user than current. Here is how I do it:
current_user = request.user # don't matter how I retrieve it
query = (Message.select(Message, User)
.where(
(Message.sender == current_user) |
(Message.receiver == current_user))
.join(User, on=(User.id == Message.other_user(current_user))))
This query works well - i.e. it retrieves the exact information I need.
But here is the problem: "other user" information is always saved as sender field.
If I use this with models which have no direct ForeignKey reference then peewee creates a new field (in this case it would be named user) for additional requested model. But if there is at least one ForeignKey relationship from primary model to secondary requested model then it uses first such relationship.
Is it possible to somehow override this behaviour?
I tried Model.alias() method, but it (unlike Node.alias) doesn't allow to specify name.
I'm not completely sure what you want, so I'll provide a snippet that will likely work for your specific scenario, and will hopefully also allow you to learn how to do what you want, if this isn't it:
SenderUser = User.alias()
ReceiverUser = User.alias()
OtherUser = User.alias()
query = (Message.select(Message, SenderUser, ReceiverUser)
.where(
(Message.sender == current_user) |
(Message.receiver == current_user))
.join(SenderUser, on = (Message.sender == SenderUser.id).alias('sender'))
.switch(Message)
.join(ReceiverUser, on = (Message.receiver == ReceiverUser.id).alias('receiver'))
.switch(Message)
.join(OtherUser, on = (Message.other_user(current_user) == OtherUser.id).alias('other_user'))
Notes:
You don't really need to create all those aliases (SenderUser/ReceiverUser/OtherUser), just two, and use User for the other. I just find that the query becomes more readable like this.
When you define an alias in the on clause, you basically tell peewee in which variable to store the joined table. I'm sending them directly to the already existing properties (sender/receiver). Also, I'm creating an extra property in the model with the value of the other user, which you can access as usual with self.other_user.
That switch method switches the current context to Message, so you can join a new table to Message instead of the SenderUser/ReceiverUser contexts where you end up after the two first joins.
If for some reason you're joining something that might be undefined (which doesn't seem to be the case here as both users are likely mandatory), you would probably want to add that you want a left outer join, like this:
.join(ReceiverUser, JOIN.LEFT_OUTER, on = (Message.receiver == ReceiverUser.id).alias('receiver'))
Don't forget to from peewee import JOIN
Something else I just noticed, is that you likely want to change that other_user method you have to compare ids instead of the model variables. If self.sender is not filled when you access it, peewee will trigger a database select to get it, so your other_user method possibly triggers 2 select queries. I would do it like:
#hybrid_method
def other_user_id(self, user):
if user.id == self.sender_id:
return self.receiver_id
elif user.id == self.receiver_id:
return self.sender_id
else:
raise ValueError
You can see that I use sender_id instead of sender.id. That uses the ids for each foreign key that are already set in the message model. If you did self.receiver.id you would likely trigger that select anyway, to then access the id property (I'm not 100% sure here though).
I'm working on a voting app, where the user can upload a list of email addresses for all of the voters. After doing some error checking, I create a Voter entity for each voter. Since there can be a large number of voters, I create the Voter entities in a taskqueue to avoid the 30 second limit and the task looks like this:
put_list = []
for email, id in itertools.izip(voter_emails, uuids):
put_list.append(Voter(election = election,
email = email,
uuid = id))
election.txt_voters = ""
put_list.append(election)
db.put(put_list)
This task, however, isn't idempotent. Is there a way to make this task idempotent? Or is there a better way to do this?
use a key_name rather than a uuid property to prevent creating duplicate voter entities.
Basically what Im trying to make is a data structure where it has the users name, id, and datejoined. Then i want a "sub-structure" where it has the users "text" and the date it was modified. and the user will have multiple instances of this text.
class User(db.Model):
ID = db.IntegerProperty()
name = db.StringProperty()
datejoined = db.DateTimeProperty(auto_now_add=True)
class Content(db.Model):
text = db.StringProperty()
datemod= db.DateTimeProperty(auto_now_add = True)
Is the code set up correctly?
One problem you will have is that making User.ID unique will be non-trivial. The problem is that two writes to the database could occur on different shards, both check at about the same time for existing entries that match the uniqueness constraint and find none, then both create identical entries (with regard to the unique property) and then you have an invalid database state. To solve this, appengine provides a means of ensuring that certain datastore entities are always placed on the same physical machine.
To do this, you make use of the entity keys to tell google how to organize the entities. Lets assume you want the username to be unique. Change User to look like this:
class User(db.Model):
datejoined = db.DateTimeProperty(auto_now_add=True)
Yes, that's really it. There's no username since that's going to be used in the key, so it doesn't need to appear separately. If you like, you can do this...
class User(db.Model):
datejoined = db.DateTimeProperty(auto_now_add=True)
#property
def name(self):
return self.key().name()
To create an instance of a User, you now need to do something a little different, you need to specify a key_name in the init method.
someuser = User(key_name='john_doe')
...
someuser.save()
Well, really you want to make sure that users don't overwrite each other, so you need to wrap the user creation in a transaction. First define a function that does the neccesary check:
def create_user(username):
checkeduser = User.get_by_key_name(username)
if checkeduser is not None:
raise db.Rollback, 'User already exists!'
newuser = User(key_name=username)
# more code
newuser.put()
Then, invoke it in this way
db.run_in_transaction(create_user, 'john_doe')
To find a user, you just do this:
someuser = User.get_by_key_name('john_doe')
Next, you need some way to associate the content to its user, and visa versa. One solution is to put the content into the same entity group as the user by declaring the user as a parent of the content. To do this, you don't need to change the content at all, but you create it a little differently (much like you did with User):
somecontent = Content(parent=User.get_by_key_name('john_doe'))
So, given a content item, you can look up the user by examining its key:
someuser = User.get(somecontent.key().parent())
Going in reverse, looking up all of the content for a particular user is only a little trickier.
allcontent = Content.gql('where ancestor is :user', user=someuser).fetch(10)
Yes, and if you need more documentation, you can check here for database types and here for more info about your model classes.
An alternative solution you may see is using referenceproperty.
class User(db.Model):
name = db.StringProperty()
datejoined = db.DateTimeProperty(auto_now_add=True)
class Content(db.Model):
user = db.ReferenceProperty(User,collection_name='matched_content')
text = db.StringProperty()
datemod= db.DateTimeProperty(auto_now_add = True)
content = db.get(content_key)
user_name = content.user.name
#looking up all of the content for a particular user
user_content = content.user.matched_content
#create new content for a user
new_content = Content(reference=content.user)