FlaskAdmin : triggering a function when clicking SAVE button - python

I'd like to add a bunch of program anytime the user is adding data to the model, after clicking the save button (or delete / edit).
In my program, one column is filled by raw SQL queries. So before inserting the record in the model, I'd like to program to test the query to make sure the SQL language has been correctly imputed.
here is the model:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
db = SQLAlchemy(app)
admin = Admin(app, template_mode='bootstrap3')
class rGraphs(db.Model):
__tablename__ = 'rGraphs'
graph_id = db.Column(db.Integer, primary_key=True)
Description = db.Column(db.Text, nullable=True)
TABLE_SRC = db.Column(db.String(50), nullable=True)
G_QUERY = db.Column(db.Text, nullable=True)
colfilters = db.relationship('tblParamColFilter', backref='rGraphs', lazy='dynamic')
coltransfos = db.relationship('tblParamColformat', backref='rGraphs', lazy='dynamic')
graphlayouts = db.relationship('tblParamGraphLayout', backref='rGraphs', lazy='dynamic')
def __repr__(self):
return self.Description
class rGraphsView(ModelView):
form_columns = ['graph_id', 'Description', 'TABLE_SRC','G_QUERY']
admin.add_view(rGraphsView(rGraphs, db.session, name='Graphics list'))
if __name__ == '__main__':
#si modele doit etre mis a jour
#manager.run()
app.run(debug=True)

You need to override the following methods in your inherited class rGraphsView: update_model(self, form, model), create_model(self, form) and delete_model(self, model).
A simple example:
class rGraphsView(ModelView):
form_columns = ['graph_id', 'Description', 'TABLE_SRC','G_QUERY']
def allow_update(self, form, model):
# test your form data and/or existing model data here
print form.G_QUERY.data
print model.G_QUERY
# return True or False
return True
def allow_create(self, form):
# test your form data here
print form.G_QUERY.data
# return True or False
return True
def allow_delete(self, model):
# test your model data here
print model.G_QUERY
# return True or False
return True
def update_model(self, form, model):
if allow_update(form, model):
# passes our allow_update test call super method
return super(rGraphsView, self).update_model(form, model)
else:
flash('Your failed update message here', 'error')
def create_model(self, form):
if allow_create(form):
# passes our allow_create test call super method
return super(rGraphsView, self).create_model(form)
else:
flash('Your failed create message here', 'error')
def delete_model(self, model):
if allow_delete(model):
# passes our allow_delete test call super method
return super(rGraphsView, self).delete_model(model)
else:
flash('Your failed delete message here', 'error')

Related

flask admin edit child objects on click

Flask-Admin shows child objects defined by relationships in its standard edit view. For example, if User objects have Address children, looking at the edit view for User will show the Address child in the appropriate field. The user can then remove the object, or add another one.
I want users to be able to click through, or otherwise have the ability to enter the edit view of child objects. In the example I'm describing, the user should be able to access the edit view of the Address object directly from the edit view of the User object.
The only thing I've found at all related is inline_models, but this isn't a solution. The implementation is extremely fragile (it can't handle long distance relationships, for example). Flask-Admin is aware of child objects! I can see them in the view! I just want them to become a link to their own edit view...
Anyone have any idea how to accomplish this or can link to an example?
Here is a single file simple example of placing a link to another model's edit view in an edit view. It may help you or not.
I've used a User - Address relationship, a user has an address and address can have many users.
I've used Faker to generate sample data so you'll need to pip install faker into your environment.
The idea is to use Flask-Admin form rules and in this case I'm configuring form_edit_rules.
I've created two custom rules:
Link, inheriting BaseRule. The constructor takes three values; an endpoint, a name of an attribute to pass along with the endpoint in the Flask url_for method and finally the text to appear as the link. In this example the endpoint is 'address.edit_view' because this is the view we want to link to.
MultiLink, similar to Link accepts it works with a relation.
Here's the code (there's little error checking):
from random import randint
from flask import Flask, url_for
from flask_admin.contrib import sqla
from flask_admin import Admin
from flask_admin.form.rules import BaseRule
from faker import Faker
from flask_sqlalchemy import SQLAlchemy
from markupsafe import Markup
from sqlalchemy import func, select
from sqlalchemy.ext.hybrid import hybrid_property
fake = Faker()
# Create application
app = Flask(__name__)
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
# Create in-memory database
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
# app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
# Flask views
#app.route('/')
def index():
return 'Click me to get to Admin!'
class Address(db.Model):
__tablename__ = 'addresses'
id = db.Column(db.Integer, primary_key=True)
number = db.Column(db.String(255))
street = db.Column(db.String(255))
city = db.Column(db.String(255))
country = db.Column(db.String(255))
#hybrid_property
def user_count(self):
return len(self.users)
#user_count.expression
def user_count(cls):
return select([func.count(User.id)]).where(User.address_id == cls.id).label("user_count")
def __unicode__(self):
return ', '.join(filter(None, [self.number, self.street, self.city, self.country]))
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(255))
last_name = db.Column(db.String(255))
email = db.Column(db.String(254))
address_id = db.Column(db.Integer, db.ForeignKey('addresses.id'), index=True)
address = db.relationship(Address, backref=db.backref('users'))
def __str__(self):
return unicode(self).encode('utf-8')
def __unicode__(self):
return '{} {}'.format(self.first_name, self.last_name)
class Link(BaseRule):
def __init__(self, endpoint, attribute, text):
super(Link, self).__init__()
self.endpoint = endpoint
self.text = text
self.attribute = attribute
def __call__(self, form, form_opts=None, field_args=None):
if not field_args
field_args = {}
_id = getattr(form._obj, self.attribute, None)
if _id:
return Markup('{text}'.format(url=url_for(self.endpoint, id=_id), text=self.text))
class MultiLink(BaseRule):
def __init__(self, endpoint, relation, attribute):
super(MultiLink, self).__init__()
self.endpoint = endpoint
self.relation = relation
self.attribute = attribute
def __call__(self, form, form_opts=None, field_args=None):
if not field_args
field_args = {}
_hrefs = []
_objects = getattr(form._obj, self.relation)
for _obj in _objects:
_id = getattr(_obj, self.attribute, None)
_link = 'Edit {text}'.format(url=url_for(self.endpoint, id=_id), text=str(_obj))
_hrefs.append(_link)
return Markup('<br>'.join(_hrefs))
class UserAdmin(sqla.ModelView):
can_view_details = True
form_edit_rules = (
'first_name',
'last_name',
'email',
'address',
Link(endpoint='address.edit_view', attribute='address_id', text='Edit Address')
)
class AddressAdmin(sqla.ModelView):
can_view_details = True
column_list = ['number', 'street', 'city', 'country', 'user_count', 'users']
form_edit_rules = (
'number',
'street',
'city',
'country',
'users',
MultiLink(endpoint='user.edit_view', relation='users', attribute='id')
)
admin = Admin(app, template_mode="bootstrap3")
admin.add_view(UserAdmin(User, db.session))
admin.add_view(AddressAdmin(Address, db.session))
def build_db():
db.drop_all()
db.create_all()
for _ in range(0, 20):
_users = []
for _ in range(0, randint(1, 10)):
_user = User(
first_name=fake.first_name(),
last_name=fake.last_name(),
email=fake.safe_email(),
)
_users.append(_user)
_address = Address(
number=fake.random_digit_not_null(),
street=fake.secondary_address(),
city=fake.city(),
country=fake.country(),
users = _users
)
db.session.add(_address)
db.session.commit()
#app.before_first_request
def first_request():
build_db()
if __name__ == '__main__':
app.run(port=5000, debug=True)

How to perform Validation on Flask-admin fields

I want to perform some validation on Flask Admin. The Flask app is connected to a backend postgreSQL DB and is using SQLAlchemy ORM.
I want to be able to perform validation checks on two fields (lan_nics, wan_nics) in the ServerView(flask.ext.admin.contrib.sqla.modelView). Basically I want to ensure the sum of two fields are not greater than a certain number. Here is the form:
ServerView
Here is the test_app code. I've tried to follow the documentation for flask-admin. I added a def and then referenced that def in the form_args for validators dictionary... but it doesn't seem to work. At the moment I'm just trying to ensure that both of the fields have a value less than 5.
from flask import Flask
from flask.ext.admin import Admin
from flask.ext.admin.contrib.sqla import ModelView
from flask.ext.sqlalchemy import SQLAlchemy
from wtforms.validators import ValidationError
import psycopg2
# Flask and Flask-SQLAlchemy initialization here
app = Flask(__name__)
db = SQLAlchemy(app)
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://postgres:postgres#127.0.0.1/testdb'
app.secret_key = 'MySecret'
conn = psycopg2.connect("dbname='testdb' user='postgres' host='127.0.0.1' password='postgres'")
class Server(db.Model):
__tablename__ = "server"
id = db.Column('server_id', db.Integer, primary_key=True)
model = db.Column('model', db.Unicode)
lan_nics = db.Column('lan_nics', db.Integer)
wan_nics = db.Column('wan_nics', db.Integer)
def __str__(self):
return self.model
class Hardware(db.Model):
__tablename__ = "hardware"
id = db.Column('hardware_id', db.Integer, primary_key=True)
model = db.Column('model', db.Unicode)
max_nics = db.Column('max_nics', db.Integer)
def __str__(self):
return self.model
class ServerView(ModelView):
# Disable model creation
can_create = True
# Override displayed fields
column_list = ('model', 'lan_nics', 'wan_nics')
column_display_all_relations = True
column_searchable_list = ('model')
column_labels = dict(model='Model', lan_nics='LAN Nics', wan_nics='WAN NICs')
form_columns = [ 'model', 'lan_nics', 'wan_nics' ]
def max_allowed(form, field):
if field.data > 5:
raise ValidationError('Max number of interfaces exceeded')
form_args = dict(
lan_nics=dict(validators=[max_allowed]),
wan_nics=dict(validators=[max_allowed])
)
class HardwareView(ModelView):
# Disable model creation
can_create = True
# Override displayed fields
column_list = ('model', 'max_nics')
column_display_all_relations = True
column_searchable_list = ('model')
column_labels = dict(model='Model', max_nics='Max Nics')
form_columns = ['model', 'max_nics' ]
def __str__(self):
return self.model
admin = Admin(app, name="MyApp1")
# Add administrative views here
admin.add_view(ModelView(Server, db.session))
admin.add_view(ModelView(Hardware, db.session))
app.run(port=8000, host="0.0.0.0")
Instead of form_args, I did on_model_change:
class ServerView(ModelView):
...
...
...
def on_model_change(self, form, model, is_created):
hardware = "hardware"
query = "SELECT nics FROM hardware WHERE model = '{}'".format(hardware)
with conn.cursor() as cur:
cur.execute(query)
max_nics = int(cur.fetchone()[0])
if (int(form.lan.data) + int(form.wan.data) > max_nics):
raise ValidationError('Max number of interfaces exceeded!')
else:
return model
Actually, your first approach works. You have a small typo: instead of if field.data > 5: you should check for if len(field.data) > 5:.
The documentation of flask-admin does not really cover validators but under the hood, it does just delegate the work to WTForms. Their documentation is very helpful: https://wtforms.readthedocs.io/en/3.0.x/validators/ It also shows existing validators such as Length, you could have written instead:
from wtforms.validators import Length
form_args = dict(
lan_nics=dict(
validators=[Length(min=5, message='Max number of interfaces exceeded')]
),
)

flask using POST, PUT, DELETE

I keep getting an error on the url when I try to implement POST in my API. I keep getting the error in the URL saying METHOD not Allowed for this URL. Am I missing something? Does POST not work directly when u try to open the server?? I'm soooo lost.
from flask import Flask, jsonify,json, request, abort
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config.from_pyfile('Config.py')
db = SQLAlchemy(app)
db.create_all()
class JsonModel(object):
def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
class User(db.Model, JsonModel):
User_ID = db.Column(db.Integer, primary_key = True)
FirstName = db.Column(db.String(20))
LastName = db.Column(db.String(20))
def __init__(self,FirstName, LastName):
self.FirstName = FirstName
self.LastName = LastName
class Todo(db.Model, JsonModel):
todo_ID = db.Column(db.Integer, primary_key = True)
UserID = db.Column(db.Integer, db.ForeignKey('User_ID'))
details = db.Column(db.String(30))
def __init__(self,details):
self.details = details
#app.route('/', methods = ['GET'])
def index():
return json.dumps([u.as_dict() for u in User.query.all()+Todo.query.all()])
#app.route('/todo/<int:UserID>', methods = ['GET'])
def get(UserID):
return (list[Todo.query.get(UserID)])
#app.route('/p/', methods = ['POST'])
def create_dev():
if not request.json or not 'name' in request.json:
abort(400)
dev = Todo(request.json.details,request.json.get('details',''))
db.session.add(dev)
db.session.commit()
return json.dumps([{'dev': dev}]), 201
if __name__ == '__main__':
app.run()
You should add GET method to list of allowed methods. When You try to load page, You first need to get page itself using GET method. Then, after filling something on the page, You use POST method to pass some data to the app. In the app, You should check with which method function is called. Something like this:
#app.route('/p', methods=['GET', 'POST'])
def create_dev():
if request.method == 'GET':
return render_template('p_page.html')
# If You get to this line, it means it is POST method
do_something_here()

Update MongoDB ReferenceField using Flask-Admin

I'm trying to create an admin page from which I can edit what roles a user is member of using MonogDB and Flask-Admin.
models.py
class Role(db.Document, RoleMixin):
name = db.StringField(max_length=80, unique=True)
description = db.StringField(max_length=255)
def __unicode__(self):
return self.name
class User(db.Document, UserMixin):
email = db.StringField(max_length=255)
password = db.StringField(max_length=255)
roles = db.ListField(db.ReferenceField(Role))
admin.py
class UserView(ModelView):
from wtforms.fields import SelectMultipleField
from bson import ObjectId, DBRef
form_overrides = dict(roles=SelectMultipleField)
options = [(g.id, g.name) for g in models.Role.objects()]
# print options
# [(ObjectId('54a72849426c702850d01921'), u'community'),
# (ObjectId('54a72849426c702850d01922'), u'customer')]
form_args = dict(roles=dict(choices=options))
When I select a user role in the Flask-Admin edit_form view and cilck save, following form validation error is shown: '54a72849426c702850d01922' is not a valid choice for this field
What's the correct way to edit/update a ReferenceField ?
Your models look fine. But your ModelView is the problem. I'm using MongoEngine and here is my implementation for them.
class Role(db.Document, RoleMixin):
name = db.StringField(max_length=80, unique=True)
description = db.StringField(max_length=255)
def __unicode__(self):
return self.name
class User(db.Document, UserMixin):
email = db.StringField(max_length=255)
password = db.StringField(max_length=500)
active = db.BooleanField(default=True)
confirmed_at = db.DateTimeField()
roles = db.ListField(db.ReferenceField(Role), default=[])
# Optional to override save method.
def save(self, *args, **kwargs):
self.password = encrypt_password(self.password) # You can encrypt your password before storing in db, as a good practice.
self.confirmed_at = datetime.now()
super(User, self).save(*args, **kwargs)
Here are my model view:
class UserView(ModelView):
can_create = True
can_delete = True
can_edit = True
decorators = [login_required]
column_filters = ('email',)
def is_accessible(self):
return current_user.has_role("admin")
class RoleView(ModelView):
can_create = True
can_delete = True
can_edit = True
decorators = [login_required]
def is_accessible(self):
return current_user.has_role("admin")
You don't have to get all Roles objects explicitly, flask-admin would do it for you. You just have to create Roles first before creating User Object.
Also, you can create an initial user by using flask's before_first_request like this:
#app.before_first_request
def before_first_request():
user_datastore.find_or_create_role(name='admin', description='Administrator')
encrypted_password = encrypt_password('password') # Put in your password here
if not user_datastore.get_user('user#example.com'):
user_datastore.create_user(email='user#example.com', password=encrypted_password)
user_datastore.add_role_to_user('user#example.com', 'admin')
This would help you in updating references correctly.

How to pass data from a QuerySelectField in flask?

In my application i have a QuerySelectField to populate a dropdown menu.
I get the choices details for the queryselectfield from the db. Once user select any choice from dropdown menu and click on Submit button which is a POST method, i want to pass the value that user selected from the dropdown to a db to store. But it always return the value None from the queryselectfield. So db stores the data as None.
models.py
class Orders(db.Model):
__tablename__ = 'orders'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64))
email = db.Column(db.String(120))
resturant = db.Column(db.String(64))
food = db.Column(db.String(64)))
forms.py
def possible_res():
return Resturant.query.all()
def possible_menu():
return Menu.query.all()
class OrderForm(Form):
sel_res = QuerySelectField(query_factory=possible_res,
get_label='name')
sel_menu = QuerySelectField(query_factory=possible_menu,
get_label='food',
allow_blank=False
)
submit = SubmitField("Confirm")
views.py
#app.route('/resturant', methods=['GET','POST'])
def resturant():
form = OrderForm()
if request.method == 'GET':
test = form.sel_menu.data
return render_template("make_order.html", form=form, test=test)
else:
a = User.query.filter_by(email = session['email']).all()
for u in a:
name = u.firstname
b = Orders(name=name, email=session['email'])
b.resturant = form.sel_res.data
b.food = form.sel_menu.data
db.session.add(b)
db.session.commit()
return redirect('/')
QuerySelectField#__init__'s query_factory argument should return a SQLAlchemy Query object, not a Python list. WTForms will materialize the query itself, by iterating over it. Simply change your factory functions to return the query attribute, rather than the list:
def possible_res():
return Resturant.query
def possible_menu():
return Menu.query

Categories

Resources