I am using Python library flask-rest-jsonapi to design an API. The library uses flask-sqlalchemy and marshmallow. Marshmallow has a requirement to have an "id" and a "type" field mandatory in the schema.
The database tables that I am designing APIs for doesn't have "id" column. The two tables that I am trying to define the relationship are called Department and Teacher where one department has multiple teachers so there is one to many relationship between department and teacher. Their primary keys columns are called "Department_Unique_ID" and "Teacher_Unique_ID".
Here is my code:
from flask import Flask
from flask_rest_jsonapi import Api, ResourceDetail, ResourceList, ResourceRelationship
from flask_rest_jsonapi.exceptions import ObjectNotFound
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound
from marshmallow_jsonapi.flask import Schema, Relationship
from marshmallow_jsonapi import fields
import connectDB
# Create the Flask application
app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app = connectDB.connectToDb(app)
db = SQLAlchemy(app)
class Department(db.Model):
db.Model.metadata.schema = 'UNIVERSITY'
__tablename__ = 'Department'
Department_Unique_ID = db.Column(db.String, primary_key=True)
Department_Name = db.Column(db.String)
uni_teacher = db.relationship('Teacher', backref=db.backref('Department'))
__mapper_args__ = {
"order_by":Department_Unique_ID
}
class Teacher(db.Model):
db.Model.metadata.schema = 'UNIVERSITY'
__tablename__ = 'Teacher'
Teacher_Unique_ID = db.Column(db.String, primary_key=True)
Teacher_Name = db.Column(db.String)
Department_ID_Unique = db.Column(db.String, db.ForeignKey('Department.Department_Unique_ID'))
__mapper_args__ = {
"order_by":Teacher_Unique_ID
}
class DepartmentSchema(Schema):
class Meta:
type_ = 'uni_department'
self_view = 'uni_department_detail'
self_view_kwargs = {'id': '<Department_Unique_ID>'}
self_view_many = 'uni_department_list'
strict = True
__model__ = Department
id = fields.Integer(as_string=True, many=True, dump_only=True) #not used, if I remove this, I get an error "Must have an 'id' field"
Department_Unique_ID = fields.Str()
Department_Name = fields.Str()
uni_teacher = Relationship(self_view='uni_department_uni_teacher',
self_view_kwargs={'id':'<Department_Unique_ID>'},
related_view='uni_teacher_list',
related_view_kwargs={'id':'<Department_Unique_ID>'},
many=True,
schema='TeacherSchema',
type_='uni_teacher',
id_field='Teacher_Unique_ID')
class TeacherSchema(Schema):
class Meta:
type_ = 'uni_teacher'
self_view = 'uni_teacher_detail'
self_view_kwargs = {'id': '<Teacher_Unique_ID>'}
self_view_many = 'uni_teacher_list'
strict = True
__model__ = Teacher
id = fields.Integer(as_string=True, many=True, dump_only=True) #not used, if I remove this, I get an error "Must have an 'id' field"
Teacher_Unique_ID = fields.Str()
Teacher_Name = fields.Str()
owner = Relationship(attribute='uni_department',
self_view='uni_teacher_uni_department',
self_view_kwargs={'id':'<Teacher_Unique_ID>'},
related_view='uni_department_detail',
related_view_kwargs={'id':'<Department_Unique_ID>'},
schema='DepartmentSchema',
type_='uni_department')
class DepartmentList(ResourceList):
schema = DepartmentSchema
data_layer = {'session': db.session,
'model': Department}
class TeacherList(ResourceList):
def query(self, view_kwargs):
query_ = self.session.query(Teacher)
if view_kwargs.get('Teacher_Unique_ID') is not None:
try:
self.session.query(Department).filter_by(Teacher_Unique_ID=view_kwargs['Teacher_Unique_ID']).one()
except NoResultFound:
raise ObjectNotFound({'parameter': 'Teacher_Unique_ID'}, "Teacher: {} not found".format(view_kwargs['Teacher_Unique_ID']))
else:
query_ = query_.join(Department).filter(Department.id == view_kwargs['Teacher_Unique_ID'])
return query_
def before_create_object(self, data, view_kwargs):
if view_kwargs.get('Teacher_Unique_ID') is not None:
uni_department = self.session.query(Department).filter_by(Teacher_Unique_ID=view_kwargs['Department_Unique_ID']).one()
data['Department_Unique_ID'] = uni_department.Department_Unique_ID
schema = TeacherSchema
data_layer = {'session': db.session,
'model': Teacher}
class DepartmentDetail(ResourceDetail):
def before_get_object(self, view_kwargs):
if view_kwargs.get('Teacher_Unique_ID') is not None:
try:
uni_teacher = self.session.query(Teacher).filter_by(Department_Unique_ID=view_kwargs['Department_Unique_ID']).one()
except NoResultFound:
raise ObjectNotFound({'parameter': 'Department_Unique_ID'},
"Teacher: {} not found".format(view_kwargs['Department_Unique_ID']))
else:
if uni_teacher.uni_department is not None:
view_kwargs['Department_Unique_ID'] = uni_teacher.uni_department.Department_Unique_ID
else:
view_kwargs['Department_Unique_ID'] = None
schema = DepartmentSchema
data_layer = {'session': db.session,
'model': Department}
class TeacherDetail(ResourceDetail):
schema = TeacherSchema
data_layer = {'session': db.session,
'model': Teacher}
class DepartmentRelationship(ResourceRelationship):
schema = DepartmentSchema
data_layer = {'session': db.session,
'model': Department}
class TeacherRelationship(ResourceRelationship):
schema = TeacherSchema
data_layer = {'session': db.session,
'model': Teacher}
api = Api(app)
api.route(DepartmentList, 'uni_department_list', '/uni_department')
api.route(TeacherList, 'uni_teacher_list', '/uni_teacher', '/uni_teacher/<int:Department_Unique_ID>/uni_teacher')
api.route(DepartmentDetail, 'uni_department_detail', '/uni_department/<int:Department_Unique_ID>', '/uni_teacher/<int:Department_ID_Unique>/owner')
api.route(TeacherDetail, 'uni_teacher_detail', '/uni_teacher/<int:Teacher_Unique_ID>')
api.route(DepartmentRelationship, 'uni_department_uni_teacher', '/uni_department/<int:Department_Unique_ID>/relationships/uni_teacher')
api.route(TeacherRelationship, 'uni_teacher_uni_department', '/uni_teacher/<int:Teacher_Unique_Department>/relationships/owner')
if __name__ == '__main__':
# Start application
app.debug = True
app.run()
I am following the example from flask-rest-jsonapi. Any help would be appreciated.
EDIT1- Here is the connectDB.py code as requested-
import yaml
def readDbDetails():
with open('database_config.yaml', 'r') as stream:
return (yaml.load(stream))
dbDetails = readDbDetails()
def connectToDb(app):
database_host = dbDetails['database_host']
database_username = dbDetails['database_username']
database_name = dbDetails['database_name']
database_password = dbDetails['database_password']
database_port = dbDetails['database_port']
print(database_host)
print(database_name)
print(database_username)
print(database_password)
print(database_port)
app.config[
'SQLALCHEMY_DATABASE_URI'] = 'mssql+pymssql://'+database_username+':'+database_password+'#'+database_host+':'+database_port+'/'+database_name+''
return app
You can use attribute property and assign the primary key to that id field
class DepartmentSchema(Schema):
class Meta:
type_ = 'uni_department'
self_view = 'uni_department_detail'
self_view_kwargs = {'id': '<Department_Unique_ID>'}
self_view_many = 'uni_department_list'
strict = True
__model__ = Department
id = fields.Integer(attribute='Department_Unique_ID', as_string=True, many=True, dump_only=True)
Department_Unique_ID = fields.Str()
Department_Name = fields.Str()
uni_teacher = Relationship(self_view='uni_department_uni_teacher',
self_view_kwargs={'id': '<Department_Unique_ID>'},
related_view='uni_teacher_list',
related_view_kwargs={'id': '<Department_Unique_ID>'},
many=True,
schema='TeacherSchema',
type_='uni_teacher',
id_field='Teacher_Unique_ID')
class TeacherSchema(Schema):
class Meta:
type_ = 'uni_teacher'
self_view = 'uni_teacher_detail'
self_view_kwargs = {'id': '<Teacher_Unique_ID>'}
self_view_many = 'uni_teacher_list'
strict = True
__model__ = Teacher
id = fields.Integer(attribute='Teacher_Unique_ID', as_string=True, many=True, dump_only=True)
Teacher_Unique_ID = fields.Str()
Teacher_Name = fields.Str()
owner = Relationship(attribute='uni_department',
self_view='uni_teacher_uni_department',
self_view_kwargs={'id': '<Teacher_Unique_ID>'},
related_view='uni_department_detail',
related_view_kwargs={'id': '<Department_Unique_ID>'},
schema='DepartmentSchema',
type_='uni_department')
Related
#app.route("/comment/<int:id>", methods=['POST'])
#jwt_required()
def create_comment(id):
current_user = get_jwt_identity()
comment = request.form['comment']
if not comment:
return jsonify('Comment cannot be empty.')
if not Recipe.select().where(Recipe.id == id).exists():
return 'Recipe ID does not Exist', 404
recipe_id = Recipe.get(id)
new_comment = Comment.create(recipe_id = recipe_id ,text=comment,
poster_id=current_user )
return jsonify('Comment posted'), 201
#app.route("/like/<int:id>", methods=['GET'])
#jwt_required()
def like(id):
current_user = get_jwt_identity()
recipe_id = Recipe.get(id)
like = Like.select().where((Recipe.poster_id == current_user) & (Recipe.id ==
id))
if not Recipe.select().where(Recipe.id == id).exists():
return jsonify('Recipe ID does not Exist'), 404
elif like:
like = Like.delete().where((Recipe.poster_id == current_user) &
(Recipe.id == id))
like.execute()
else:
like = Like.create(recipe_id = recipe_id , poster_id=current_user )
return jsonify(f'You have liked the recipe with ID: {recipe_id}'), 200
#app.route("/dislike/<int:id>", methods=['GET'])
#jwt_required()
def dislike(id):
current_user = get_jwt_identity()
get_recipe_id = Recipe.get(id)
if not Recipe.select().where(Recipe.id == id).exists():
return 'Recipe ID does not Exist', 404
dislike = Dislike.create(recipe_id = get_recipe_id , poster_id=current_user )
return jsonify(f'You have disliked the recipe with ID: {get_recipe_id}'), 200
When i try the like route i get the following error:
peewee.OperationalError: (1054, "Unknown column 't2.poster_id' in 'where clause'")
This route works the way i want it when i query an ID present in the database. but When i try using a random ID that is not present in the db, i get the following error:
model.RecipeDoesNotExist: <Model: Recipe> instance matching query does not exist:
SQL: SELECT `t1`.`id`, `t1`.`name`, `t1`.`description`,
`t1`.`ingredients`, `t1`.`process`,
`t1`.`post_date`, `t1`.`poster_id`, `t1`.`image` FROM `recipe` AS `t1`
WHERE (`t1`.`id` = %s) LIMIT %s
OFFSET %s
Params: [22, 1, 0]
I am using peewee and MySQL. Below is the structure of my model. The database is in good shape as all my tables take data but the error output is from PostMan.
class BaseModel(Model):
class Meta:
database = db
class Users(BaseModel):
id = PrimaryKeyField(primary_key=True)
fullname = CharField()
username = CharField()
email = CharField()
password_harsh = CharField()
birthday = DateField()
gender = CharField()
#property
def password(self):
raise AttributeError('Password is not readabale attribure!')
#password.setter
def password(self, password):
self.password_harsh = generate_password_hash(password)
def verify_password(self,password):
return check_password_hash(self.password_harsh, password)
class Recipe(BaseModel):
id = PrimaryKeyField(primary_key=True)
name = CharField()
description = CharField()
ingredients = CharField()
process = TextField()
post_date = DateTimeField(constraints=[SQL('DEFAULT CURRENT_TIMESTAMP')])
poster_id = ForeignKeyField(Users, backref='recipe', lazy_load=False)
image = CharField()
class Comment(BaseModel):
id = PrimaryKeyField(primary_key=True)
text = CharField()
recipe_id = ForeignKeyField(Recipe, backref='comment', lazy_load=False)
post_date = DateTimeField(constraints=[SQL('DEFAULT CURRENT_TIMESTAMP')])
poster_id = ForeignKeyField(Users, backref='comment', lazy_load=False)
class Like(BaseModel):
id = PrimaryKeyField(primary_key=True)
recipe_id = ForeignKeyField(Recipe, backref='comment', lazy_load=False)
post_date = DateTimeField(constraints=[SQL('DEFAULT CURRENT_TIMESTAMP')])
poster_id = ForeignKeyField(Users, backref='like', lazy_load=False)
class Dislike(BaseModel):
id = PrimaryKeyField(primary_key=True)
recipe_id = ForeignKeyField(Recipe, backref='comment', lazy_load=False)
post_date = DateTimeField(constraints=[SQL('DEFAULT CURRENT_TIMESTAMP')])
poster_id = ForeignKeyField(Users, backref='dislike', lazy_load=False)
For your second error, you're getting a DoesNotExist exception because as you explained you're looking for an item that doesn't exist in your database. If you want to handle the error correctly, you should do something like this:
try:
Recipe.get_by_id(9999)
except Recipe.DoesNotExist:
# Handle it here
Or you could also do it like so:
Recipe.get_or_none(Recipe.id == 9999)
Which will return None if the recipe doesn't exist.
I'm not sure this answers your question as I am not sure what your question is in the first place.
I am trying to troubleshoot my python database by posting an image to it. However whenever I send the post request I receive the following error.
AttributeError: 'NoneType' object has no attribute 'read'
line 98, in add_file
new_file = File(name, file_type, data.read())
AttributeError: 'NoneType' object has no attribute 'read'
Here's the code I have as of now.
from flask import Flask, request, jsonify, send_file
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
from flask_cors import CORS
from flask_heroku import Heroku
import io
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "postgres://wbzxqshctunral:df393ee4891d46d62fdfa0daf99f0cd5ba4563268340386d5caf85904f470c71#ec2-34-224-229-81.compute-1.amazonaws.com:5432/drj57s4cb9qkq"
db = SQLAlchemy(app)
ma = Marshmallow(app)
heroku = Heroku(app)
CORS(app)
class VehicleInformation(db.Model):
id = db.Column(db.Integer, primary_key=True)
make = db.Column(db.String(), nullable=False)
model = db.Column(db.String(), nullable=False)
mpgE = db.Column(db.Integer, nullable=False)
year = db.Column(db.Integer, nullable=False)
description = db.Column(db.String(), nullable=False)
def __init__(self, make, model, mpgE, year, description):
self.make = make
self.model = model
self.mpgE = mpgE
self.year = year
self.description = description
class VehicleInformationSchema(ma.Schema):
class Meta:
fields = ("id", "make", "model", "mpgE", "year", "description")
vehicle_information_schema = VehicleInformationSchema()
multiple_vehicle_information_schema = VehicleInformationSchema(many=True)
class File(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(), nullable=False)
file_type = db.Column(db.String(), nullable=False)
data = db.Column(db.LargeBinary, nullable=False)
def __init__(self, name, file_type, data):
self.name = name
self.file_type = file_type
self.data = data
class FileSchema(ma.Schema):
class Meta:
fields = ("id", "name", "file_type")
file_schema = FileSchema()
files_schema = FileSchema(many=True)
#app.route("/vehicle/add", methods=["POST"])
def add_vehicle():
if request.content_type != "application/json":
return jsonify("Error: Data must be sent as JSON")
vehicle_data = request.get_json()
make = vehicle_data.get("make")
model = vehicle_data.get("model")
mpgE = vehicle_data.get("mpgE")
year = vehicle_data.get("year")
description = vehicle_data.get("description")
record = VehicleInformation(make, model, mpgE, year, description)
db.session.add(record)
db.session.commit()
return jsonify("Vehicle has been added successfully")
#app.route("/vehicle/all_vehicles", methods=["GET"])
def get_all_vehicles():
all_vehicles = db.session.query(VehicleInformation).all()
return jsonify(multiple_vehicle_information_schema.dump(all_vehicles))
#app.route("/vehicle/delete", methods=["DELETE"])
def delete_vehicle(id):
vehicle_data = db.session.query(VehicleInformation).filter(VehicleInformation.id == id).first()
db.session.delete(vehicle_data)
db.session.commit()
return jsonify("Vehicle has been eradicated")
#app.route("/file/add", methods=["POST"])
def add_file():
name = request.form.get("name")
file_type = request.form.get("type")
data = request.files.get("data")
new_file = File(name, file_type, data.read())
db.session.add(new_file)
db.session.commit()
return jsonify("File added successfully")
#app.route("/file/get/data", methods=["GET"])
def get_file_data():
file_data = db.session.query(File).all()
return jsonify(files_schema.dump(file_data))
#app.route("/file/get/<id>", methods=["GET"])
def get_file(id):
file_data = db.session.query(File).filter(File.id == id).first()
return send_file(io.BytesIO(file_data.data),
attachment_filename=file_data.name,
mimetype=file_data.file_type)
#app.route("/file/delete/<id>", methods=["DELETE"])
def delete_file(id):
file_data = db.session.query(File).filter(File.id == id).first()
db.session.delete(file_data)
db.session.commit()
return jsonify("File Deleted Successfully")
if __name__ == "__main__":
app.run(debug=True)
I appreciate the help in advance!
It seems like you aren't handling error cases where, for example, data isn't passed to the request:
#app.route("/file/add", methods=["POST"])
def add_file():
name = request.form.get("name")
file_type = request.form.get("type")
data = request.files.get("data")
if data is None:
return jsonify("Missing file!")
# You should probably have the same checkers for name and file_type
new_file = File(name, file_type, data.read())
db.session.add(new_file)
db.session.commit()
return jsonify("File added successfully")
I'm writing a simple app for adding and updating information to sqlite3 database using Flask-SQLAlchemy. Now I have a big problem, because I cannot update the database, it just adds a new record every time I submit the form button.
Here is my code:
#main.route("/invoices", methods=["GET", "POST"], defaults={"invoice_id": None})
#main.route("/invoices/<int:invoice_id>", methods=["GET", "POST"])
def invoices(invoice_id):
invoice = None
if invoice_id:
invoice = Invoice.query.get_or_404(invoice_id)
if request.method == "POST":
date = request.form["date"]
name = request.form["name"]
value = request.form["value"]
currency = request.form["currency"]
payment = request.form["payment"]
category = request.form["category"]
description = request.form["description"]
if invoice:
invoice.date = datetime.strptime(date, "%Y-%m-%d")
invoice.name = name
invoice.value = value
invoice.currency = currency
invoice.payment = payment
invoice.category = category
invoice.description = description
else:
invoice = Invoice(
date=datetime.strptime(date, "%Y-%m-%d"),
name=name,
value=value,
currency=currency,
payment=payment,
category=category,
description=description,
)
db.session.add(invoice)
db.session.commit()
return redirect(url_for("main.invoices", invoice_id=invoice.id))
currencies = Currency.query.all()
payments = Payment.query.all()
categories = Category.query.all()
context = {
"invoice_id": invoice_id,
"currencies": currencies,
"payments": payments,
"categories": categories,
"invoice": invoice,
}
return render_template("invoices.html", **context)
I don't know how to update the data, because it always adds a new record, now just commits the existing or changed fields. Maybe something wrong is with my if statements...
Please help me with this task...
Here is the models:
class Invoice(db.Model):
id = db.Column(db.Integer, primary_key=True)
value = db.Column(db.Integer)
date = db.Column(db.DateTime)
name = db.Column(db.String(50))
description = db.Column(db.String(100))
currency = db.Column(db.ForeignKey("currency.id"))
payment = db.Column(db.ForeignKey("payment.id"))
category = db.Column(db.ForeignKey("category.id"))
class Currency(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(10))
class Payment(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
This code may be help to you
#main.route("/invoices", methods=["GET", "POST"], defaults={"invoice_id": None})
#main.route("/invoices/<int:invoice_id>", methods=["GET", "POST"])
def invoices(invoice_id):
invoice = None
if invoice_id is not None:
invoice = Invoice.query.filter_by(id=invoice_id).first()
if request.method == "POST":
date = request.form["date"]
name = request.form["name"]
value = request.form["value"]
currency = request.form["currency"]
payment = request.form["payment"]
category = request.form["category"]
description = request.form["description"]
if invoice is not None:
invoice.date = datetime.strptime(date, "%Y-%m-%d")
invoice.name = name
invoice.value = value
invoice.currency = currency
invoice.payment = payment
invoice.category = category
invoice.description = description
else:
invoice = Invoice(
date=datetime.strptime(date, "%Y-%m-%d"),
name=name,
value=value,
currency=currency,
payment=payment,
category=category,
description=description,
)
db.session.add(invoice)
db.session.commit()
return redirect(url_for("main.invoices", invoice_id=invoice.id))
currencies = Currency.query.all()
payments = Payment.query.all()
categories = Category.query.all()
context = {
"invoice_id": invoice_id,
"currencies": currencies,
"payments": payments,
"categories": categories,
"invoice": invoice,
}
return render_template("invoices.html", **context)
i'm creating an API in python + Flask + marshmallow.
Here's the class + the first function (i don't already use the others )
import datetime
from marshmallow import Schema, fields, ValidationError, pre_load
from flask import Flask, jsonify
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy
from flask import request
import os
app = Flask(__name__)
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'User.sqlite')
# Order matters: Initialize SQLAlchemy before Marshmallow
db = SQLAlchemy(app)
ma = Marshmallow(app)
class User(db.Model):
userid = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30))
badgenum = db.Column(db.String(30))
mail = db.Column(db.String(30))
status = db.Column(db.String(30))
admin = db.Column(db.String(30))
def __init__(self, userid, name, badgenum, mail, status, admin):
self.userid = userid
self.name = name
self.badgenum = badgenum
self.mail = mail
self.status = status
self.admin = admin
class UserSchema(ma.Schema):
class Meta:
fields = ('userid', 'name', 'badgenum', 'mail', 'status', 'admin')
user_schema = UserSchema()
users_schema = UserSchema(many=True)
#app.route("/User", methods=["POST"])
def add_User():
userid = request.json['userid']
name = request.json['name']
badgenum = request.json['badgenum']
mail = request.json['mail']
status = request.json['status']
admin = request.json['admin']
new_user = User(userid, name, badgenum, mail, status, admin)
db.session.add(new_user)
db.session.commit()
return jsonify(new_user)
I tested the function add_User using Postman with this json request :
{
"userid" : 1,
"name" : "name1",
"badgenum" : "66897",
"mail" : "ghh#orange.fr",
"status" : "on",
"admin" : "f"
}
and i got this error:
TypeError: <User 145> is not JSON serializable
User has its own autogenerated id user.id which is ObjectId.
You need custom json encoder to encode ObjectId field.
import json, bson
def json_response(obj, cls=None):
response = make_response(json.dumps(obj, cls=cls))
response.content_type = 'application/json'
return response
class MongoJsonEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, bson.ObjectId):
return str(obj)
return json.JSONEncoder.default(self, obj)
Now try to call return json_response(new_user, cls=MongoJsonEncoder)
I have three related SQLAlchemy models in my flask-admin application (simplified):
class Client(db.Model, BasicMixin, ActiveMixin, TimestampMixin):
id = db.Column(UUIDType, primary_key=True, default=uuid.uuid4)
title = db.Column(db.String(1000))
issues = db.relationship('Issue', backref='client', cascade='all, delete-orphan')
class Issue(db.Model, BasicMixin, ActiveMixin, TimestampMixin):
id = db.Column(UUIDType, primary_key=True, default=uuid.uuid4)
date = db.Column(db.Date, default=datetime.date.today())
client_id = db.Column(UUIDType, db.ForeignKey('clients.id'), nullable=False)
articles = db.relationship('Article', backref='issue', cascade='all, delete-orphan')
class Article(db.Model, BasicMixin, TimestampMixin):
id = db.Column(UUIDType, primary_key=True, default=uuid.uuid4)
title = db.Column(db.String())
body = db.Column(db.String())
issue_id = db.Column(UUIDType, db.ForeignKey('issues.id'), nullable=False)
Client has many Issues, each Issue has many Articles.
I also have a ModelView for Article in which I should be able to filter list of Articles by Client (select Client by name and show Articles belonging to this Client only). What should I do to create such a filter in flask-admin?
Here's a single-file example using SQLite:
Mostly straightforward Flask, SQLalchemy and Flask-Admin. The class of interest is FilterByClientTitle.
class FilterByClientTitle(BaseSQLAFilter):
# Override to create an appropriate query and apply a filter to said query with the passed value from the filter UI
def apply(self, query, value, alias=None):
return query.join(Article.issue).join(Issue.client).filter(Client.title == value)
# readable operation name. This appears in the middle filter line drop-down
def operation(self):
return u'equals'
# Override to provide the options for the filter - in this case it's a list of the titles of the Client model
def get_options(self, view):
return [(client.title, client.title) for client in Client.query.order_by(Client.title)]
The view for the Article model has a couple of important settings/overrides:
class ArticleView(BaseAdminView):
# ......
# No need to specify the column as we'll set the SQLalchemy filter directly in the filter's apply method
column_filters = [FilterByClientTitle(column=None, name='Client Title')]
# Need this so the filter options are always up-to-date
#expose('/')
def index_view(self):
self._refresh_filters_cache()
return super(ArticleView, self).index_view()
Here's the complete example, requires the faker library for the random data:
import datetime
from flask import Flask, url_for
from flask_admin.contrib.sqla import ModelView
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin, expose
from faker import Faker
from flask_admin.contrib.sqla.filters import BaseSQLAFilter
from markupsafe import Markup
app = Flask(__name__)
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///sample_db.sqlite'
db = SQLAlchemy(app)
#app.route('/')
def index():
return 'Click me to get to Admin!'
class Client(db.Model):
__tablename__ = 'clients'
id = db.Column(db.Integer(), primary_key=True)
title = db.Column(db.String(1000))
def __str__(self):
return unicode(self).encode('utf-8')
def __unicode__(self):
return self.title
class Issue(db.Model):
__tablename__ = 'issues'
id = db.Column(db.Integer(), primary_key=True)
date = db.Column(db.Date, default=datetime.date.today())
client_id = db.Column(db.Integer(), db.ForeignKey('clients.id'), nullable=False)
client = db.relationship(Client, backref=db.backref('issues', uselist=True, cascade='all, delete-orphan'))
def __str__(self):
return unicode(self).encode('utf-8')
def __unicode__(self):
return unicode(self.date)
class Article(db.Model):
__tablename__ = 'articles'
id = db.Column(db.Integer(), primary_key=True)
title = db.Column(db.String())
body = db.Column(db.String())
issue_id = db.Column(db.Integer(), db.ForeignKey('issues.id'), nullable=False)
issue = db.relationship(Issue, backref=db.backref('articles', uselist=True, cascade='all, delete-orphan'))
def __str__(self):
return unicode(self).encode('utf-8')
def __unicode__(self):
return '{title} ... {body} ...'.format(title=self.title[:30], body=self.body[:30])
class BaseAdminView(ModelView):
can_view_details = True
named_filter_urls = True
class ClientView(BaseAdminView):
column_list = ('id', 'title')
column_default_sort = ('title', False)
column_filters = ['id', 'title']
class IssueView(BaseAdminView):
column_list = ('id', 'date', 'articles')
column_default_sort = ('date', False)
column_filters = ['id', 'date']
column_formatters = {
'articles': lambda v, c, m, n: Markup('<br>'.join([unicode(a) for a in m.articles])),
}
class FilterByClientTitle(BaseSQLAFilter):
# Override to create an appropriate query and apply a filter to said query with the passed value from the filter UI
def apply(self, query, value, alias=None):
return query.join(Article.issue).join(Issue.client).filter(Client.title == value)
# readable operation name. This appears in the middle filter line drop-down
def operation(self):
return u'equals'
# Override to provide the options for the filter - in this case it's a list of the titles of the Client model
def get_options(self, view):
return [(client.title, client.title) for client in Client.query.order_by(Client.title)]
class ArticleView(BaseAdminView):
column_list = ('title', 'body', 'issue', 'issue.client')
column_labels = {
'issue': 'Issue Date',
'issue.client': 'Client Title'
}
column_default_sort = ('title', False)
def issue_link(self, context, model, name):
return Markup('{date}'.format(
url=url_for('issue.index_view', flt1_id_equals=model.issue.id),
date=model.issue.date)
)
def client_link(self, context, model, name):
return Markup('{title}'.format(
url=url_for('client.index_view', flt1_id_equals=model.issue.client.id),
title=model.issue.client.title)
)
# Display Issue Date and Client Title as links back to their filtered views
column_formatters = {
'title': lambda v, c, m, n: '{} ...'.format(m.title[:20]),
'body': lambda v, c, m, n: '{} ...'.format(m.body[:40]),
'issue': issue_link,
'issue.client': client_link,
}
# No need to specify the column as we'll set the SQLalchemy filter directly in the filter's apply method
column_filters = [FilterByClientTitle(column=None, name='Client Title')]
# Need this so the filter options are always up-to-date
#expose('/')
def index_view(self):
self._refresh_filters_cache()
return super(ArticleView, self).index_view()
admin = Admin(app, template_mode="bootstrap3")
admin.add_view(ClientView(Client, db.session))
admin.add_view(IssueView(Issue, db.session))
admin.add_view(ArticleView(Article, db.session))
def build_sample_db():
fake = Faker()
number_of_clients = 100
number_of_issues_per_client = 5
number_of_articles_per_issues = 5
db.drop_all()
db.create_all()
clients = []
issues = []
articles = []
for client_counter in range(0, number_of_clients):
client_title = fake.last_name()
clients.append({
'id': client_counter,
'title': client_title
})
for issue_counter in range(0, number_of_issues_per_client):
issue_id = number_of_issues_per_client * client_counter + issue_counter
issues.append({
'id': issue_id,
'client_id': client_counter,
'date': fake.date_time_this_decade(before_now=True, after_now=False, tzinfo=None)
})
for article_counter in range(0, number_of_articles_per_issues):
articles.append({
'id': (number_of_articles_per_issues * issue_id) + article_counter,
'issue_id': issue_id,
'title': '{} - {}'.format(client_title, fake.catch_phrase()),
'body': '{} - {}'.format(client_title, fake.text(max_nb_chars=200))
})
db.session.bulk_insert_mappings(Client, clients)
db.session.bulk_insert_mappings(Issue, issues)
db.session.bulk_insert_mappings(Article, articles)
db.session.commit()
if __name__ == '__main__':
build_sample_db()
app.run(debug=True)