SQALCHEMY - query filtering by attribute and user_id - flask-sqlalchemy

I have a User model, and a Playlist model, which references user_id as a foreign key
models.py
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
playlists = db.relationship("Playlist",
backref=db.backref('user'),
uselist=True)
def serialize(self):
"""Return object data in easily serializeable format"""
return {
'id': self.id,
'playlists' : self.playlists}
class Playlist(db.Model):
"""
Model for storing playlist information belonging to a specific user
"""
__tablename__ = 'playlist'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(50))
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
#property
def serialize(self):
"""Return object data in easily serializeable format"""
return {
'id' : self.id,
'created' : self.created,
'title': self.title,
}
methods.py
def Upload_Tracks(dataset):
try:
playlist = Playlist.query.filter(Playlist.title == 'Cache').first()
if not playlist:
# create one
playlist = Playlist(title='Cache',
user=User.query.get(1))
db.session.add(playlist)
db.session.commit()
db.session.commit()
else:
playlist = Playlist.query.filter(
and_(Playlist.title == 'Cache', User.id == 1)).first()
# handler errors
except (exc.IntegrityError, ValueError):
db.session.rollback()
return {"status": True}
At first iteration of, playlist is created ADDED NEW <Playlist 'Cache'>, but at second iteration, when code reaches else, I'm getting AttributeError: 'NoneType' object for playlist here:
playlist = Playlist.query.filter(
and_(Playlist.title == 'Cache', User.id == 1)).first()
If I query filtering only one attribute, like so:
playlist = Playlist.query.filter(
(Playlist.title == 'Cache')).first()
it works.
So how do I query playlist by title AND user_id at the same time?

You get the above error because you tried to call User.id attribute but you didn't join that model (table) with Playlist model (table).
SQL has JOINs to join two tables (LEFT, RIGHT, INNERJOIN ...).
SQLAlchemy which you use handle this raw SQL by using join method.
So you have to use join() method to join two tables (models).
playlist = Playlist.query.join(User, User.id == Playlist.user_id).filter(
and_(Playlist.title == 'Cache', User.id == 1)).first()
You will get playlist as None (if there is no row like this in db) or Object if there is row like this.

Related

How write a Django REST APIView POST request with a condition?

I am trying to create a post request for a game api. The game implies that a user can label a picture. A label entered once is a tagging, a label entered twice for the same resource is a tag.
This is how I am trying to create a Tagging so far:
saved_tagging = Tagging.objects.create(user_id=current_user_id,
gameround=gameround,
resource=random_resource,
tag='tag newwww',
created=datetime.now(),
score=score,
origin=origin
)
tagging_serializer = TaggingSerializer(saved_tagging)
At the moment I am getting the ValueError: Cannot assign "'tag newwww'": "Tagging.tag" must be a "Tag" instance.
Is there any way that I can avoid this?
Here are also my models and the relevant serializer.
models.py
class Tag(models.Model):
name = models.CharField(max_length=256)
language = models.CharField(max_length=256)
objects = models.Manager()
def create(self, validated_data):
tag_data = validated_data.pop('tag')
Tag.objects.create(**tag_data)
return tag_data
def __str__(self):
return self.name or ''
class Tagging(models.Model):
user = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, null=True)
gameround = models.ForeignKey(Gameround, on_delete=models.CASCADE, related_name='taggings')
resource = models.ForeignKey(Resource, on_delete=models.CASCADE, related_name='taggings')
tag = models.ForeignKey(Tag, on_delete=models.CASCADE, related_name='tagging')
created = models.DateTimeField(editable=False)
score = models.PositiveIntegerField(default=0)
origin = models.URLField(max_length=256, blank=True, default='')
objects = models.Manager()
def create(self, validated_data):
tag_data = validated_data.pop('tag')
tagging = Tagging.objects.create(**validated_data)
Tag.objects.create(name=tagging, **tag_data)
return tagging
def __str__(self):
return str(self.tag) or ''
serializers.py
class TaggingSerializer(serializers.ModelSerializer):
tag = StringRelatedField()
resource = ResourceSerializer(read_only=True)
gameround = GameroundSerializer(read_only=True)
class Meta:
model = Tagging
fields = ('id', 'tag', 'gameround', 'created', 'score', 'resource', 'origin')
def create(self, validated_data):
return Tagging.objects.create(**validated_data)
def to_representation(self, data):
data = super().to_representation(data)
return data
tag must be a Tag Instance !
So... you can do this in two ways (in my opinion).
First you can create a Tag object in your view and than pass this object to tag value in your Tagging create method.
Or create a service layer on your app, and create a custom create method for your model. This in my opinion is superior because your a centralizing your rules in one method instead of in one view.
Ex.:
services/tag_service.py
def create(user_id,gameround,resource,tag,...origin):
if not isinstance(tag, Tag):
#create your tag model based on the String passed
your_new_tag_object = Tag.objects.create(tag=tag, ...)
# Here your create others rules too for model creation
# Return your model .create method
return Tagging.objects.create(user_id=user_id,...tag=your_new_tag_object,...)
And Than use this new create method inside your POST serializer.
from services import tag_service
class TaggingSerializer(serializers.ModelSerializer):
# your normal serializer here
def create(self, validated_data):
return tag_service.create(**validated_data)

How to remove SQLAlchemy Many-To-Many Orphans from database?

Context
I have a simple MySQL database written with SQLAlchemy. The following are my two models, Subreddit and Keyword, that have a many-to-many relationship, along with their association table:
subreddits_keywords = db.Table('subreddits_keywords', db.Model.metadata,
db.Column('subreddit_id', db.Integer, db.ForeignKey('subreddits.id', ondelete='CASCADE')),
db.Column('keyword_id', db.Integer, db.ForeignKey('keywords.id', ondelete='CASCADE')),
)
class Subreddit(db.Model, JsonSerializer):
__tablename__ = 'subreddits'
id = db.Column(db.Integer, primary_key=True)
subreddit_name = db.Column(db.String(128), index=True)
# Establish a parent-children relationship (subreddit -> keywords).
keywords = db.relationship('Keyword', secondary=subreddits_keywords, backref='subreddits', cascade='all, delete', passive_deletes=True, lazy='dynamic')
// ...
class Keyword(db.Model, JsonSerializer):
__tablename__ = 'keywords'
id = db.Column(db.Integer, primary_key=True)
keyword = db.Column(db.String(128), index=True)
// ...
As test data, I've created the following data set:
Subreddit:
test_subreddit
Keywords:
test_keyword1
test_keyword2
test_keyword3
In other words, test_subreddit.keywords should return [test_keyword1, test_keyword2, test_keyword3].
Problem
When I remove test_subreddit, test_keyword1, test_keyword2, test_keyword3 still persist in the database.
I understand that with many-to-many relationships, there is technically no parent so cascade's technically will not work according to this post:
https://stackoverflow.com/a/803584/10426919.
What I've Tried
I followed this link: https://github.com/sqlalchemy/sqlalchemy/wiki/ManyToManyOrphan.
This link provides a library function that should fix my exact problem.
However, the function does not work when integrated into my Model file in the following ways:
Method #1:
from app.extensions import db
from werkzeug.security import generate_password_hash, check_password_hash
from sqlalchemy.inspection import inspect
from sqlalchemy_utils import auto_delete_orphans <------ # library
subreddits_keywords = db.Table('subreddits_keywords', db.Model.metadata,
db.Column('subreddit_id', db.Integer, db.ForeignKey('subreddits.id', ondelete='CASCADE')),
db.Column('keyword_id', db.Integer, db.ForeignKey('keywords.id', ondelete='CASCADE')),
)
class Subreddit(db.Model, JsonSerializer):
__tablename__ = 'subreddits'
id = db.Column(db.Integer, primary_key=True)
subreddit_name = db.Column(db.String(128), index=True)
# Establish a parent-children relationship (subreddit -> keywords).
keywords = db.relationship('Keyword', secondary=subreddits_keywords, backref='subreddits', cascade='all, delete', passive_deletes=True, lazy='dynamic')
// ...
class Keyword(db.Model, JsonSerializer):
__tablename__ = 'keywords'
id = db.Column(db.Integer, primary_key=True)
keyword = db.Column(db.String(128), index=True)
// ...
auto_delete_orphans(Subreddit.keywords) <------ # Library function
However, this function does not seem to do anything. There is no error that is output to help guide me towards the right direction. When I check my database in MySQL workbench, the Subreddit, test_subreddit, is deleted, but the keywords [test_keyword1, test_keyword2, test_keyword3] are still in the database under the Keywords table.
Method #2:
I tried integrating the actual function, that the library function is based on, into my code as well:
from app.extensions import db
from werkzeug.security import generate_password_hash, check_password_hash
from sqlalchemy.inspection import inspect
from sqlalchemy_utils import auto_delete_orphans
# for deleting many-to-many "orphans".
from sqlalchemy import event, create_engine
from sqlalchemy.orm import attributes, sessionmaker
subreddits_keywords = db.Table('subreddits_keywords', db.Model.metadata,
db.Column('subreddit_id', db.Integer, db.ForeignKey('subreddits.id', ondelete='CASCADE')),
db.Column('keyword_id', db.Integer, db.ForeignKey('keywords.id', ondelete='CASCADE')),
)
class Subreddit(db.Model, JsonSerializer):
__tablename__ = 'subreddits'
id = db.Column(db.Integer, primary_key=True)
subreddit_name = db.Column(db.String(128), index=True)
# Establish a parent-children relationship (subreddit -> keywords).
keywords = db.relationship('Keyword', secondary=subreddits_keywords, backref='subreddits', cascade='all, delete', passive_deletes=True, lazy='dynamic')
// ...
class Keyword(db.Model, JsonSerializer):
__tablename__ = 'keywords'
id = db.Column(db.Integer, primary_key=True)
keyword = db.Column(db.String(128), index=True)
// ...
engine = create_engine("mysql://", echo=True)
Session = sessionmaker(bind=engine)
#event.listens_for(Session, 'after_flush')
def delete_tag_orphans(session, ctx):
# optional: look through Session state to see if we want
# to emit a DELETE for orphan Tags
flag = False
for instance in session.dirty:
if isinstance(instance, Subreddit) and \
attributes.get_history(instance, 'keywords').deleted:
flag = True
break
for instance in session.deleted:
if isinstance(instance, Subreddit):
flag = True
break
# emit a DELETE for all orphan Tags. This is safe to emit
# regardless of "flag", if a less verbose approach is
# desired.
if flag:
session.query(Keyword).\
filter(~Keyword.subreddits.any()).\
delete(synchronize_session=False)
Again, the keywords persisted despite being attached to no parent.
What I'm trying to accomplish
When children in the database no longer have a parent, I would like them to be removed from the database. What am I doing wrong?
Rather than using auto_delete_orphans, I created a method that I can call when I want to delete children. This method checks the child in question, and sees if it has any parents. If it does have a parent, we leave it be, but if it does not have a parent, we then delete the children.
Here is how I implemented this method, given that a Subreddit is a parent and a Keyword is a child of Subreddit.
def check_for_keyword_orphans(keyword):
# check if each keyword has an associated subreddit
if len(keyword.subreddits) == 0:
db.session.delete(keyword)
return True # keyword deleted
else:
return False # keyword has an associated subreddit
And here is how I used the method in my API route:
keywords = subreddit.keywords
for keyword in keywords:
check_for_keyword_orphans(keyword)
db.session.commit()

Edit many-to-many relationship in Flask-SQLAlchemy withg WTForms

I struggle handling a many-to-many relationship (here: users and groups) in a Flask form. My database structure (simplified) looks as follow:
association_user_group = db.Table(
'association_user_group',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('group_id', db.Integer, db.ForeignKey('group.id'))
)
class Group(db.Model):
__tablename__ = 'group'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(32))
users = db.relationship(
'User',
secondary=association_user_group,
backref=db.backref('groups', lazy='dynamic'))
class User(UserMixin, db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
#property
def group_ids(self):
return [u.id for u in self.groups]
To handle a form that allows an admin to edit the users, I have:
class MultiCheckboxField(SelectMultipleField):
widget = widgets.ListWidget(prefix_label=False)
option_widget = widgets.CheckboxInput()
class UserEditForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password')
groups = MultiCheckboxField('Groups', coerce=int)
submit = SubmitField('Apply changes')
and the following route:
#bp.route('/user_edit/<int:id>', methods=['GET', 'POST'])
#login_required
#admin_required
def user_edit(id):
user = User.query.get(id)
if request.method == 'GET':
form = UserEditForm(obj=user)
form.groups.data = [grp.id for grp in user.groups]
else:
form = UserEditForm(request.form)
form.groups.choices = [(grp.id, grp.name) for grp in Group.query.all()]
if form.validate_on_submit():
#form.populate_obj(user) <-- does not work
user.username = form.data.username
if form.password.data != '':
user.set_password(form.password.data)
# how do I update the 'groups'?
db.session.add(user)
db.session.commit()
return "Data={}".format(form.data)
The database and form works, but I am unable to copy the groups form content back to the database. Ideally, I'd be able to 'populate back' the form content to the User object, but this fails because of the groups (and I think it might also fail with the password field that is empty if no change is requested).
I then tried to delete all groups from user and re-add the ones I want, but did not figure out a reasonable way to achieve this. I am able to add an group membership with user.groups.append(grp) where grp is the corresponding database object. I also am able to remove a group membership in the same way, but given the group ids this would mean looping through all group ids, retrieving the group object, and then using this object to invoke the remove method, which seems overly complicated.
Overall, I suspect that I attempt to implements all this in a far too awkward way...

How to update a db row from an action in Flask-appbuilder and SQLAInterface

I am building a app using flask-appbuilder and I have an action that runs a function and I want to update the row in the table with the output of function. Can't work out how to do it. Any help? Thanks
#action("prospect", "Prospect", "off we go", "fa-rocket")
def prospect(self, items):
if isinstance(items, list):
for a in items:
out = self.myfunction(a.name)
#Need to update the table with output
#anyideas?
self.update_redirect()
else:
print "nothing"
return redirect(self.get_redirect())
I'm assuming this is a View that is related to a model. If this is the case, you relate the model to the view using Flask-AppBuilder SQLAInterface class.
This class allows you to interact with the item in the database.
This class has an 'edit' method that let's you update the item.
Let's say your model is like the one below:
class Contact(Model):
id = Column(Integer, primary_key=True)
name = Column(String(50), unique = True, nullable=False)
Let's say you want to implement an action that capiltalizes the contact name, and you want to be able to do it on the the 'List' and 'Show' views. This is one way to do it:
class ContactView(ModelView):
datamodel = SQLAInterface(Contact)
#action("capitalize",
"Capitalize name",
"Do you really want to capitalize the name?")
def capitalize(self, contacts):
if isinstance(contacts, list):
for contact in contacts:
contact.name = contact.name.capitalize()
self.datamodel.edit(contact)
self.update_redirect()
else:
contacts.name = contacts.name.capitalize()
self.datamodel.edit(contacts)
return redirect(self.get_redirect())
You can check other SQLAInterface methods here.

Filter query with variable list in django

I have a three table model like that :
I want to filter results of a query with a variable list of items for exemple :
listTags = ["landscape","green"]
results = ListTag.objects.filter(tag__name__in=listTags).select_related()
But the result of that query is all the ListTag objects with landscape OR green but what i want it's a list of ListTag objects with landscape AND green
I saw a lot a of answers about that problem but a lot of them use a static list of tags, what i want it's to filter with a variable listtags list
Edit : the model
class Picture(models.Model):
title = models.CharField(max_length=50,null=True, blank=False, verbose_name=('name'))
def __str__(self):
return self.titre
class Tag(models.Model):
name = models.CharField(max_length=50,null=True, blank=False, verbose_name=('name'))
def __str__(self):
return self.name
class ListTags(models.Model):
picture = models.ForeignKey(Picture, blank=False, on_delete=models.CASCADE, related_name='picture')
tag = models.ForeignKey(Tag, blank=False, on_delete=models.CASCADE, related_name='tag')
You can try to use Django Q object.
In your case this could be:
from django.db.models import Q
...
listTags = ["landscape","green"]
query = Q()
for tag in listTags:
query &= Q(tag__name = tag)
results = ListTag.objects.filter(query).select_related()
addition:
if you want just pictures with tags, then you could use many-to-many relationships. But if you want use tags for different types of models, then u need to use generic relations.
In first case models structure could be:
from django.db import models
class Tag(models.Model):
name = models.CharField(max_length=50, null=True, blank=False, verbose_name=('name'))
def __str__(self):
return self.name
class Picture(models.Model):
title = models.CharField(max_length=50, null=True, blank=False, verbose_name=('name'))
tags = models.ManyToManyField(Tag)
def __str__(self):
return self.title
With m2m relation Q object will not work, so to get all pictures with landscape and green tags you can use filter chaining:
listTags = ["landscape", "green"]
results = models.Picture.objects.all()
for tag in listTags:
results = results.filter(tags__name = tag)
I believe the code below would make this an AND query rather than an OR query:
listTags = ["landscape","green"]
filters = {}
for value in listTags:
filters['tag__name__in'] = value
results = ListTag.objects.filter(**filters)