Adding a String of a filename or absolute path to a database when using Flask-Admin and SQLAlchemy - flask

I am working with SQLAlchemy and Flask-Admin right now. Let us say that I am trying to get a String of the file name or a String of the absolute path of the file name of an image file. I am trying to insert either of these into a database automatically when I create a user. The code that I am using is structured similar to this.  
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
url_pic = Column(String(50), nullable=False)
pic = Column(LargeBinary, nullable=False)
...
from flask.ext.admin.contrib.sqla import ModelView
from flask.ext.admin.form.upload import FileUploadField
from wtforms.validators import ValidationError
from flask.ext.admin import Admin
from flask.ext.sqlalchemy import SQLAlchemy
from flask import Flask
import imghdr
app = Flask(__name__)
db = SQLAlchemy(app)
class UserAdminView(ModelView):
def picture_validation(form, field):
if field.data:
filename = field.data.filename
if filename[-4:] != '.jpg':
raise ValidationError('file must be .jpg')
if imghdr.what(field.data) != 'jpeg':
raise ValidationError('file must be a valid jpeg image.')
field.data = field.data.stream.read()
return True
form_columns = ['id','url_pic', 'pic']
column_labels = dict(id='ID', url_pic="Picture's URL", pic='Picture')
def pic_formatter(view, context, model, name):
return 'NULL' if len(getattr(model, name)) == 0 else 'a picture'
column_formatters = dict(pic=pic_formatter)
form_overrides = dict(pic= FileUploadField)
form_args = dict(pic=dict(validators=[picture_validation]))
admin = Admin(app)
admin.add_view(UserAdminView(User, db.session, category='Database Administration'))
...
How could we get a string version of the file name or absolute path when using the picture_validation function? Right now, the function only provides the binary data of the file, which isn’t as useful.

Related

flask-admin different view for multiple databases

I have two different databases. They are: stock and commodity. Each database has two tables as follows:
Stock: User_trade_stock, stock_prices
Commodity: User_trade_commodity, commodity_prices
I try to build a web app to handle two databases with flask. When I apply flask-admin to them as follows
admin.add_view(UserView(User_trade_stock, db.session))
admin.add_view(UserView(User_trade_commodity, db.session))
I gives the following eror:
Assertion Error: A name collision occurred between blueprints. Blueprints that are created on the fly need unique name.
I tried to add the bind to the db.session as follows
admin.add_view(UserView(User_trade_stock, db.session(bind='stock_bind')))
admin.add_view(UserView(User_trade_commodity, db.session='commodity_bind')))
I got the following error:
scoped session is already present; no new arguments may be specified
Any helps would be appreciated
There are a couple of issues.
Flask-Admin uses a view's lower cased class name for the automatically generated Blueprint name. As you are using UserView twice you have a Blueprint name collision. To overcome this you can specify an endpoint name when you instance a view, for example:
admin = Admin(app, template_mode="bootstrap3")
admin.add_view(TestView(StockTest, db.session, category='Stock', name='Test', endpoint='stock-test'))
admin.add_view(TestView(CommodityTest, db.session, category='Commodity', name='Test', endpoint='commodity-test'))
and to get the urls of the views you would use the following code:
url_for('stock-test.index')
url_for('stock-test.edit')
url_for('commodity-test.index')
url_for('commodity-test.edit')
Secondly, if you want to use the bind feature of Flask-Sqlalchemy you should use the __bind_key__ attribute on the table model, for example:
class User(db.Model):
__bind_key__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True)
Here is a single file example illustrating both concepts. You will need to run the flask commands flask create-databases and flask populate-databases before you use the app itself. Note I've used a mixin class, TestMixin, to define the model columns.
import click
from flask import Flask
from flask.cli import with_appcontext
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
from faker import Faker
from sqlalchemy import Integer, Column, Text
db = SQLAlchemy()
app = Flask(__name__)
app.config['SECRET_KEY'] = '123456790'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.config['SQLALCHEMY_BINDS'] = {
'stock': 'sqlite:///stock.db',
'commodity': 'sqlite:///commodity.db'
}
db.init_app(app)
class TestMixin(object):
id = Column(Integer, primary_key=True)
name = Column(Text(), unique=True, nullable=False)
class StockTest(db.Model, TestMixin):
__bind_key__ = 'stock'
class CommodityTest(db.Model, TestMixin):
__bind_key__ = 'commodity'
#click.command('create-databases')
#with_appcontext
def create_databases():
db.drop_all(bind=['stock', 'commodity'])
db.create_all(bind=['stock', 'commodity'])
#click.command('populate-databases')
#with_appcontext
def populate_databases():
_faker = Faker()
db.session.bulk_insert_mappings(StockTest, [{'name': _faker.name()} for _ in range(100)])
db.session.bulk_insert_mappings(CommodityTest, [{'name': _faker.name()} for _ in range(100)])
db.session.commit()
class TestView(ModelView):
pass
app.cli.add_command(create_databases)
app.cli.add_command(populate_databases)
admin = Admin(app, template_mode="bootstrap3")
admin.add_view(TestView(StockTest, db.session, category='Stock', name='Test', endpoint='stock-test'))
admin.add_view(TestView(CommodityTest, db.session, category='Commodity', name='Test', endpoint='commodity-test'))
#app.route('/')
def index():
return 'Click me to get to Admin!'
if __name__ == '__main__':
app.run()

Flask-Migrate - 'Please edit configuratio/connection/logging settings' on 'flask db init'

Have spent literally the past three days trying to figure this out and just can't seem to get it. Have reviewed other posts here and elsewhere regarding the same issue (see here, here, here, among others) countless times and am apparently just too dense to figure it out myself. So here we are, any help would be greatly appreciated.
Project structure:
/project-path/
/app.py
/config.py
/.flaskenv
/app/__init__.py
/app/models.py
/app/routes.py
config.py
import os
from dotenv import load_dotenv
basedir = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(basedir, '.env'))
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'bleh'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
app.py
from app import create_app
app = create_app()
.flaskenv
FLASK_APP=app.py
/app/init.py
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_marshmallow import Marshmallow
from config import Config
db = SQLAlchemy()
migrate = Migrate()
ma = Marshmallow()
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
from app.models import Borrower
db.init_app(app)
migrate.init_app(app, db)
from app.routes import borrowers_api
app.register_blueprint(borrowers_api)
return app
/app/models.py
from app import db, ma
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
class Borrower(db.Model):
__tablename__ = 'borrowers'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
borrower_name = db.Column(db.String(100), nullable=False, unique=True, index=True)
def __init__(self, borrower_name):
self.borrower_name = borrower_name
class BorrowerSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Borrower
include_relationships = True
load_instance = True #Optional: deserialize to model instances
borrower_schema = BorrowerSchema()
borrowers_schema = BorrowerSchema(many=True)
/app/routes.py
from flask import Blueprint, request, jsonify
from app.models import Borrower, borrower_schema, borrowers_schema
from app import db
borrowers_api = Blueprint('borrowers_api', __name__)
# Add a borrower
#borrowers_api.route('/borrower', methods=['POST'])
def add_borrower():
borrower_name = request.get_json(force=True)['borrower_name']
new_borrower = Borrower(borrower_name)
db.session.add(new_borrower)
db.session.commit()
return borrower_schema.jsonify(new_borrower)
# Get single borrower by id
#borrowers_api.route('/borrower/<id>', methods=['GET'])
def get_borrower(id):
borrower = Borrower.query.get(id)
return borrower_schema.jsonify(borrower)
Yet all I continue to run into the following:
(env-name) C:\Users\...\flask-scratchwork\flask-restapi-tryagain>flask db init
Creating directory C:\Users\...\flask-scratchwork\flask-restapi-tryagain\migrations ... done
Creating directory C:\Users\...\flask-scratchwork\flask-restapi-tryagain\migrations\versions ... done
Generating C:\Users\...\flask-scratchwork\flask-restapi-tryagain\migrations\alembic.ini ... done
Generating C:\Users\...\flask-scratchwork\flask-restapi-tryagain\migrations\env.py ... done
Generating C:\Users\...\flask-scratchwork\flask-restapi-tryagain\migrations\README ... done
Generating C:\Users\...\flask-scratchwork\flask-restapi-tryagain\migrations\script.py.mako ... done
Please edit configuration/connection/logging settings in 'C:\\Users\\...\\flask-scratchwork\\flask-restapi-tryagain\\migrations\\alembic.ini' before proceeding.
Where am I going wrong?
Nevermind, think the answer was actually just proceed with flask db migrate despite:
Please edit configuration/connection/logging settings in 'C:\\Users\\...\\flask-scratchwork\\flask-restapi-tryagain\\migrations\\alembic.ini' before proceeding.
So for anyone else spending hours trying to discern precisely what type of edits flask-migrate wants you to make to to the "configuration/connection/logging settings" in alembic.ini...the answer is seemingly to just proceed with flask db migrate

Flask app-builder how to make REST API with file items

I'm making a REST api that files can be uploaded based in MODEL-VIEW in flask-appbuilder like this.
But I don't know how to call REST API (POST /File).
I tried several different ways. but I couldn't.
Let me know the correct or the alternative ways.
[client code]
file = {'file':open('test.txt', 'rb'),'description':'test'}
requests.post(url, headers=headers, files=file)
==> Failed
model.py
class Files(Model):
__tablename__ = "project_files"
id = Column(Integer, primary_key=True)
file = Column(FileColumn, nullable=False)
description = Column(String(150))
def download(self):
return Markup(
'<a href="'
+ url_for("ProjectFilesModelView.download", filename=str(self.file))
+ '">Download</a>'
)
def file_name(self):
return get_file_original_name(str(self.file))
view.py
class FileApi(ModelRestApi):
resource_name = "File"
datamodel = SQLAInterface(Files)
allow_browser_login = True
appbuilder.add_api(FileApi)
FileColumn is only a string field that saves the file name in the database. The actual file is saved to config['UPLOAD_FOLDER'].
This is taken care of by flask_appbuilder.filemanager.FileManager.
Furthermore, ModelRestApi assumes that you are POSTing JSON data. In order to upload files, I followed Flask's documentation, which suggests to send a multipart/form-data request. Because of this, one needs to override ModelRestApi.post_headless().
This is my solution, where I also make sure that when a Files database row
is deleted, so is the relative file from the filesystem.
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder.api import ModelRestApi
from flask_appbuilder.const import API_RESULT_RES_KEY
from flask_appbuilder.filemanager import FileManager
from flask import current_app, request
from marshmallow import ValidationError
from sqlalchemy.exc import IntegrityError
from app.models import Files
class FileApi(ModelRestApi):
resource_name = "file"
datamodel = SQLAInterface(Files)
def post_headless(self):
if not request.form or not request.files:
msg = "No data"
current_app.logger.error(msg)
return self.response_400(message=msg)
file_obj = request.files.getlist('file')
if len(file_obj) != 1:
msg = ("More than one file provided.\n"
"Please upload exactly one file at a time")
current_app.logger.error(msg)
return self.response_422(message=msg)
else:
file_obj = file_obj[0]
fm = FileManager()
uuid_filename = fm.generate_name(file_obj.filename, file_obj)
form = request.form.to_dict(flat=True)
# Add the unique filename provided by FileManager, which will
# be saved to the database. The original filename can be
# retrieved using
# flask_appbuilder.filemanager.get_file_original_name()
form['file'] = uuid_filename
try:
item = self.add_model_schema.load(
form,
session=self.datamodel.session)
except ValidationError as err:
current_app.logger.error(err)
return self.response_422(message=err.messages)
# Save file to filesystem
fm.save_file(file_obj, item.file)
try:
self.datamodel.add(item, raise_exception=True)
return self.response(
201,
**{API_RESULT_RES_KEY: self.add_model_schema.dump(
item, many=False),
"id": self.datamodel.get_pk_value(item),
},
)
except IntegrityError as e:
# Delete file from filesystem if the db record cannot be
# created
fm.delete_file(item.file)
current_app.logger.error(e)
return self.response_422(message=str(e.orig))
def pre_delete(self, item):
"""
Delete file from filesystem before removing the record from the
database
"""
fm = FileManager()
current_app.logger.info(f"Deleting {item.file} from filesystem")
fm.delete_file(item.file)
You can use this.
from app.models import Project, ProjectFiles
class DataFilesModelView(ModelView):
datamodel = SQLAInterface(ProjectFiles)
label_columns = {"file_name": "File Name", "download": "Download"}
add_columns = ["file", "description", "project"]
edit_columns = ["file", "description", "project"]
list_columns = ["file_name", "download"]
show_columns = ["file_name", "download"]
Last add the view to the menu.
appbuilder.add_view(DataFilesModelView,"File View")

Django: How to get values from my model field and use it to search for an online image, then store the image url in the model?

Using Python 3
I have a Django project with an anime app. The anime app has a model as follows:
class Anime(models.Model):
id = UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = CharField(max_length=100, blank=False)
genre = CharField(max_length=1000, blank=False)
TV = "TV/Series"
MOVIE = "Movie"
SPECIAL = "Special"
OVA = "OVA"
UNKNOWN = "Unknown"
MUSIC = "Music"
ONA = "ONA"
typeChoices = (
(TV, "TV/Series"),
(MOVIE, "Movie"),
(SPECIAL, "Special"),
(OVA, "OVA"),
(ONA, "ONA"),
(MUSIC, "Music"),
(UNKNOWN, "Unknown")
)
animeType = CharField(max_length=10, choices=typeChoices, default=TV, blank="False")
episodes = IntegerField()
rating = FloatField()
members = IntegerField()
photoCover = ImageField(null=True, upload_to="img/covers", verbose_name="cover photo", blank=True)
I'd like to get the name of the anime (data is inputted by a csv or a form input) and use it in a google search and get the first image of the search. I have this code that may help me do that.
import urllib
from bs4 import BeautifulSoup
searchName = searchName.replace(" ", "%20")
searchName = searchName.replace("-", "%2D")
searchName = searchName.replace("&", "%26")
html = urllib.urlopen("http://www.google.com/search?q="+searchName+"&tbm=isch")
soup = BeautifulSoup(html)
img_links = soup.findAll("img", {"class":"rg_ic"})
for img_link in img_links:
print(img_link.img['src'])
What I don't know is if this would work and if it would work, I don't know where to apply the code. I'd want to use img_link.img['src'] as the value for the Anime.photoCover.url as it would be in the template.
Use this below function to save url into the image field
import urllib
import os
from django.core.files import File
from . models import Anime
def create_anime_from_url(name, genre, animeType, episodes, rating, members, url):
obj = Anime.objects.create(
name=name, genre=genre, animeType=animeType,
episodes=episodes, rating=rating, members=members
)
photoCover = urllib.request.urlretrieve(url)
obj.photoCover.save(
os.path.basename(url),
# File(open(photoCover[0]))
' '
)
If you don't want to store the image url into file system then you have to use the CustomFileSystemStorage to handle the functionality.
class CustomFileSystemStorage(FileSystemStorage):
def _save(self, name, content):
if name.startswith('https://') or name.startswith('http://'):
return name
return super(CustomFileSystemStorage, self)._save(name, content)
def url(self, name):
if name.startswith('https://') or name.startswith('http://'):
return name
return super(CustomFileSystemStorage, self).url(name)
class Anime(models.Model):
# .............
photoCover = ImageField(
null=True, upload_to="img/covers",
verbose_name="cover photo", blank=True,
storage=CustomFileSystemStorage())
# .............

copy file from one model to another

I have 2 simple models:
class UploadImage(models.Model):
Image = models.ImageField(upload_to="temp/")
class RealImage(models.Model):
Image = models.ImageField(upload_to="real/")
And one form
class RealImageForm(ModelForm):
class Meta:
model = RealImage
I need to save file from UploadImage into RealImage. How could i do this.
Below code doesn't work
realform.Image=UploadImage.objects.get(id=image_id).Image
realform.save()
Tnx for help.
Inspired by Gerard's solution I came up with the following code:
from django.core.files.base import ContentFile
#...
class Example(models.Model):
file = models.FileField()
def duplicate(self):
"""
Duplicating this object including copying the file
"""
new_example = Example()
new_file = ContentFile(self.file.read())
new_file.name = self.file.name
new_example.file = new_file
new_example.save()
This will actually go as far as renaming the file by adding a "_1" to the filename so that both the original file and this new copy of the file can exist on disk at the same time.
Although this is late, but I would tackle this problem thus,
class UploadImage(models.Model):
Image = models.ImageField(upload_to="temp/")
# i need to delete the temp uploaded file from the file system when i delete this model
# from the database
def delete(self, using=None):
name = self.Image.name
# i ensure that the database record is deleted first before deleting the uploaded
# file from the filesystem.
super(UploadImage, self).delete(using)
self.Image.storage.delete(name)
class RealImage(models.Model):
Image = models.ImageField(upload_to="real/")
# in my view or where ever I want to do the copying i'll do this
import os
from django.core.files import File
uploaded_image = UploadImage.objects.get(id=image_id).Image
real_image = RealImage()
real_image.Image = File(uploaded_image, uploaded_image.name)
real_image.save()
uploaded_image.close()
uploaded_image.delete()
If I were using a model form to handle the process, i'll just do
# django model forms provides a reference to the associated model via the instance property
form.instance.Image = File(uploaded_image, os.path.basename(uploaded_image.path))
form.save()
uploaded_image.close()
uploaded_image.delete()
note that I ensure the uploaded_image file is closed because calling real_image.save() will open the file and read its content. That is handled by what ever storage system is used by the ImageField instance
Try doing that without using a form. Without knowing the exact error that you are getting, I can only speculate that the form's clean() method is raising an error because of a mismatch in the upload_to parameter.
Which brings me to my next point, if you are trying to copy the image from 'temp/' to 'real/', you will have to do a some file handling to move the file yourself (easier if you have PIL):
import Image
from django.conf import settings
u = UploadImage.objects.get(id=image_id)
im = Image.open(settings.MEDIA_ROOT + str(u.Image))
newpath = 'real/' + str(u.Image).split('/', 1)[1]
im.save(settings.MEDIA_ROOT + newpath)
r = RealImage.objects.create(Image=newpath)
Hope that helped...
I had the same problem and solved it like this, hope it helps anybody:
# models.py
class A(models.Model):
# other fields...
attachment = FileField(upload_to='a')
class B(models.Model):
# other fields...
attachment = FileField(upload_to='b')
# views.py or any file you need the code in
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
from django.core.files.base import ContentFile
from main.models import A, B
obj1 = A.objects.get(pk=1)
# You and either copy the file to an existent object
obj2 = B.objects.get(pk=2)
# or create a new instance
obj2 = B(**some_params)
tmp_file = StringIO(obj1.attachment.read())
tmp_file = ContentFile(tmp_file.getvalue())
url = obj1.attachment.url.split('.')
ext = url.pop(-1)
name = url.pop(-1).split('/')[-1] # I have my files in a remote Storage, you can omit the split if it doesn't help you
tmp_file.name = '.'.join([name, ext])
obj2.attachment = tmp_file
# Remember to save you instance
obj2.save()
Update Gerard's Solution to handle it in a generic way:
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
from django.core.files.base import ContentFile
init_str = "src_obj." + src_field_name + ".read()"
file_name_str = "src_obj." + src_field_name + ".name"
try:
tmp_file = StringIO(eval(str(init_str)))
tmp_file = ContentFile(tmp_file.getvalue())
tmp_file.name = os.path.basename(eval(file_name_str))
except AttributeError:
tmp_file = None
if tmp_file:
try:
dest_obj.__dict__[dest_field_name] = tmp_file
dest_obj.save()
except KeyError:
pass
Variable's Used:
src_obj = source attachment object.
src_field_name = source attachment object's FileField Name.
dest_obj = destination attachment object.
dest_field_name = destination attachment object's FileField Name.