flask admin edit child objects on click - python

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)

Related

FlaskAdmin : triggering a function when clicking SAVE button

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')

column_filter by grandparent's property in flask-admin

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)

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')]
),
)

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.

Google App Engine Django BooleanProperty not binding to table

I have the following,
class Company(db.Model):
companyvalid = db.BooleanProperty(required=True)
class AddCompanyForm(djangoforms.ModelForm):
class Meta:
model = Company
exclude = ['companyentrytime']
exclude = ['companylatlong']
however I cannot get the o/p from the Django stored in the database. I can also only add a record when the checkbox is checked, but this is not reflected in the underlying table when saving the record. What is the smartest way to do this? Thanks
class AddCompanyCategoriesHandler(webapp.RequestHandler):
def get(self):
memcache.flush_all()
form_requirements = AddCompanyCategoriesForm()
path = os.path.join(os.path.dirname(__file__), 'addcompanycat.html')
self.response.out.write(template.render(path, {'form': form_requirements}))
def post(self):
form_requirements = AddCompanyCategoriesForm(data=self.request.POST)
if form_requirements.is_valid():
myname = form_requirements.clean_data['categoryname']
entity = form_requirements.save(commit=False)
entity.put()
=========================================================================================
I'm trying to use the BooleanField, but this fails to work, with the server giving out a 504 error. Here is my model. I've been experimenting with this BooleanFields format, but I'm not sure how this relates to my model. My model is
class Company(db.Model):
companyurl = db.StringProperty(required=True)
companyname = db.StringProperty(required=True)
companydesc = db.TextProperty(required=True)
companyaddress = db.PostalAddressProperty(required=True)
companypostcode = db.StringProperty(required=True)
companyemail = db.EmailProperty(required=True)
companycountry = db.StringProperty(required=True)
companyvalid = db.BooleanProperty()
#companyvalid = db.BooleanField(required=True, label="Check this")
companyentrytime = db.DateTimeProperty(auto_now_add=True)
companylatlong = db.GeoPtProperty()
#property
def catname(self):
return self.companycategory.name
companycategory = db.ReferenceProperty(CompanyCategory, collection_name='compcategory')
and the following
class AddCompanyForm(djangoforms.ModelForm):
class Meta:
model = Company
#companyvalid = model.BooleanField(default=False)
exclude = ['companyentrytime']
exclude = ['companylatlong']
So my question is that if I have to use this BooleanField, how should I put it in the AddCompanyForm and should there be an entry in the model?
Try using BooleanField (https://docs.djangoproject.com/en/dev/ref/models/fields/#booleanfield) rather than Boolean Property in your model?

Categories

Resources