In my application, I have Users and Posts as models. Each post has a foreign key to a username. When I create a ModelView on top of my Posts model I can create posts as specific users in the admin interface
as seen in the screenshot below
After I have added a post and click "Save and Add Another", the "User" reverts back to "user1". How can I make the form remember the previous value "user2"?
My reserach has led me to believe it can be done by modifying on_model_change and on_form_prefill, and saving the previous value in the flask session, but it seems to be overengineering such a simple task. There must be a simpler way.
My code can be seen below
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import flask_admin
from flask_admin.contrib import sqla
app = Flask(__name__)
db = SQLAlchemy()
admin = flask_admin.Admin(name='Test')
class Users(db.Model):
"""
Contains users of the database
"""
user_id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True, nullable=False)
def __str__(self):
return self.username
class Posts(db.Model):
"""
Contains users of the database
"""
post_id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(11), db.ForeignKey(Users.username), nullable=False)
post = db.Column(db.String(256))
user = db.relation(Users, backref='user')
def build_sample_db():
db.drop_all()
db.create_all()
data = {'user1': 'post1', 'user1': 'post2', 'user2': 'post1'}
for user, post in data.items():
u = Users(username=user)
p = Posts(username=user, post=post)
db.session.add(u)
db.session.add(p)
db.session.commit()
class MyModelView(sqla.ModelView):
pass
if __name__ == '__main__':
app.config['SECRET_KEY'] = '123456790'
app.config['DATABASE_FILE'] = 'sample_db.sqlite'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database'
app.config['SQLALCHEMY_ECHO'] = True
db.init_app(app)
admin.init_app(app)
admin.add_view(MyModelView(Posts, db.session))
with app.app_context():
build_sample_db()
# Start app
app.run(debug=True)
I have come across this situation before and i have solved it using 2 functions. its pretty easy and small.
#expose('/edit/', methods=('GET', 'POST'))
def edit_view(self):
#write your logic to populate the value into html
self._template_args["arg_name"] = stored_value
# in your html find this value to populate it as you need
the above function will let you populate the values in html when user tries to edit any value. This can be populated using the value stored. And below is a function that helps you save the value from previous edit.
within this class MyModelView(sqla.ModelView): you need to add the below 2 functions.
def on_model_change(self, form, model, is_created):
stored_value = model.user # this is your user name stored
# get the value of the column from your model and save it
This is a 2 step operation that's pretty small and does not need a lot of time. I have added just a skeleton/pseudo code for now.
on_form_prefill will not help you with this problem, as it only works for the edit form.
As the lib code says -> on_form_prefill => Perform additional actions to
pre-fill the edit form
When a request hit, the below flow of code happens:
(Base class) View(create_view) ->create_form->_create_form_class->get_create_form->get_form-> scaffold_form
Hence If we want to change the default value of the create form being loaded we can update it in the methods handling the form as explained in the above flow.
Hence we can override create_form.
def create_form(self):
form = super().create_form()
# set the user id we want in the form as default.
form.user_id.default = 'USER_2_ID'
return form
This was to get the default value for the form.
To set the value we override the on_model_change
def on_model_change(self, form, model, is_created):
# set the USER_ID from this model.user_id
pass
Now the way to share this data(USER_ID) from the setter and getter methods are following,
We set this in cookies and get on the create request.
Update the "save and add another" button link to add the user_id in the query strings.
This data has to be shared between two different requests hence storing it in application context, g won't work. The application context "will not be shared between requests"
def create_form(self):
form = super().create_form()
user_id = request.args.get('user_id')
form.user_id.default = user_id
# or if you want only one option to be available
user_query = self.session.query(User).filter(User.id = user_id).one()
form.user.query = [user_query]
return form
As the above answer mention to use edit_view, create_view, that can also be used to add
An easy option but not a good one will be to query on creating
that will give you the last entry(But this approach is not optimal
one)
#expose('/new/', methods=('GET', 'POST'))
def create_view(self):
model = self.get_model()
# query the model here to get the latest value of the user_id
self._template_args['user_id'] = user_id
return super(YourAdmin, self).details_view()
Related
I am trying to create a college system using flask but I have a problem with log in system basically I have two classes Teacher class and Student class the problem is that I cant figure a way to make the login manager handle the two classes login at the same time only one can log in and when the other class try to log in it log me as the class what I mean is when I setup the login manager to accept student it works with the student but when I try to login as a teacher it searches in the student table instead of teacher table I don't know if I explained it right but i hope code snippet help
this is models
attend = db.Table('attend',
db.Column('teacher_id',db.Integer,db.ForeignKey('teacher.id')),
db.Column('student_id', db.Integer, db.ForeignKey('student.id'))
)
class Teacher(db.Model,UserMixin):
id = db.Column(db.Integer, primary_key=True)
name=db.Column(db.String(60),nullable=False)
email=db.Column(db.String(100),nullable=False,unique=True)
password=db.Column(db.String(60),nullable=False)
department=db.Column(db.String(50),nullable=False)
students=db.relationship('Student',secondary=attend,backref='students',lazy='dynamic')
def type_auth(self):
return True
def __repr__(self) -> str:
return f'name:{self.name} email:{self.email} department:{self.department}'
class Student(db.Model,UserMixin):
id = db.Column(db.Integer, primary_key=True)
name=db.Column(db.String(60),nullable=False)
email = db.Column(db.String(100), nullable=False, unique=True)
password = db.Column(db.String(60), nullable=False)
year=db.Column(db.Integer,nullable=False)
grades = db.relationship('Grades', backref='Rates', lazy='dynamic')
def type_auth(self):
return False
def __repr__(self) -> str:
return f'name:{self.name} email:{self.email} year:{self.year}'
this login manager
def create_app():
app=Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI']=f'sqlite:///{DB_NAME}'
app.config['SECERT_KEY'] = '1853d2c8983cff3b'
app.config['FLASK_ADMIN_SWITCH']='Cyborg'
db.init_app(app)
from .auth import auth
from .routes import routes
app.register_blueprint(auth,prefix_url='/')
app.register_blueprint(routes,prefix_url='/')
login_manger=LoginManager()
login_manger.login_view='auth.login'
login_manger.login_message_category='info'
login_manger.init_app(app)
#login_manger.user_loader
def user_loader(id):
#i need help here
return app
The user_loader callback received an id as an argument and from that information is expected to return an instance of whatever you have defined as your "User" class. You have effectively defined two "User" classes, and in the current configuration there is no way for the user loader, if passed "1", to determine whether it should return the Teacher with id 1 or the Student with id 1.
A better design might be to move email and password into a new User class, and create one-to-one relationships between User and Student and Teacher. You can add some validation to ensure that a User cannot relate to both a Student and a Teacher.
You can do this:
When a user logs in, search in both the teacher and student database and then log them in. While logging, create a session variable say session['teacher'] = true, to identify the role when the person has logged in.
Or
You may also try giving an identifier for the user to choose. For example: I am a: Teacher(chekcbox) Student(checkbox). Depending on the check selected, look into that db.
I'm writing a lot of boilerplate like code to try and persist my form data. In other languages I've not had to manually map the form attribute to the model prior to saving I'm wondering does python/flask has a similar approach? I'd ideally prefer to manually map all my form fields to simply persist a db record.
[edit]
To be clear this approach works fine for me the records are persisted properly however it seems kind of inefficient. In another piece of code I was able to use the wtf library {{ wtf.quick_form(form) }}
to create a form an html form without explicitly listing all its fields. Given I have the same names for my model attributes as I do for the form attributes I was wondering if I can do the same?
[edit]
Any feedback
Example below..
I have a largish model "product"
class Product(db.Model):
field1 = db.Column(db.String(200))
field2 = db.Column(db.String(200))
....
field30 = db.Column(db.String(200))
It's represented as a flask form e.g.
class ProductForm(FlaskForm):
field1 = StringField('Field 1', validators=[DataRequired()])
field2 = TextAreaField('Field 2 Label')
field30 = TextAreaField('Field 30 Label')
submit = SubmitField('Submit')
Here I try and map and persist the product record...
def save_product(self, product, form):
product.field1 = form.field.data
#lots more tedious form to model mapping
db.session.add(product)
# commit the data to the database
db.session.commit()
In my routes.py I have the following. I see it being called and the record does indeed persist.
def crud():
form = CrudForm()
if form.validate_on_submit():
product = Product()
product.save_product(product, form, new=True)
flash(_('Product Saved'))
return redirect(url_for('main.index'))
return render_template('crud.html', title=_('Product CRUD'),
form=form)
Given that the names of your form fields are the same as the names of your database columns it is possible to cut down on the boilerplate. A dict of all of the fields in your form is accessible as the form.data property and this can be passed directly through as keyword arguments when your create the new instance of your model rather than setting them one at a time afterwards.
product = Product(**form.data)
I'm trying to use search capability on flask application. It seems to be saving in database properly however query isn't returning me anything.
DATABASE MODEL:
app = Flask(__name__)
csrf = CsrfProtect(app)
csrf.init_app(app)
db = SQLAlchemy(app)
class ArticleQuery(BaseQuery, SearchQueryMixin):
pass
class latest_movies_scraper(db.Model):
query_class = ArticleQuery
__tablename__ = 'latest_movies_scraper'
id = db.Column(sa.Integer, primary_key=True)
name = db.Column(db.Unicode(255))
url = db.Column(db.Unicode(255))
image_url = db.Column(db.Unicode(255))
create = db.Column(db.DateTime, default=datetime.datetime.utcnow)
search_vector = db.Column(TSVectorType('name'))
How i'm saving to database:
check_if_exists = latest_movies_scraper.query.filter_by(name=dictionary['title']).first()
if check_if_exists:
print check_if_exists.name
print 'skipping this...'
pass
else:
insert_to_db = latest_movies_scraper(name=dictionary['title'], url=dictionary['href'], image_url=dictionary['featured_image'])
db.session.add(insert_to_db)
db.session.commit()
How I am using search capbilitiy functionality:
name = latest_movies_scraper.query.search(u'Black Panther (2018)').limit(5).all()
Name returns empty array, but it should return me the name list instead.
ABOVE MY GOAL is to query the name from the database. It doesn't return me anything when in fact the name Black Panther 2018 exists in my database.
So the search functionality isn't working as expected.
SQLAlchemy-Searchable doesn't index existing data. This has to be done manually by performing a synchronisation. For the table definition above the code below is sufficient:
from sqlalchemy_searchable import sync_trigger
def sync_fts():
sync_trigger(db.engine, 'latest_movies_scraper', 'search_vector', ['name'])
This code would normally be part of the db management tools (Flask-Script, Click).
I want to define a selection field in python, i.e. field that is limited to a set of values. How can I do that in flask framework. I could not find anything on selection fields in the following sources:
Declaring Models
SQLAlchemy in Flask
I am using sqlalchemy for ORM.
I assume you mean a field in a form that has a limited set of options; to do this you can use WTForms and its extensions which allow you to create forms from models.
Once you have done that, you can then limit the choices for a field based on a model condition.
As you haven't posted your model, here is the example give you give you an idea on how this would work:
def enabled_categories():
return Category.query.filter_by(enabled=True)
class BlogPostEdit(Form):
title = TextField()
blog = QuerySelectField(get_label='title')
category = QuerySelectField(query_factory=enabled_categories,
allow_blank=True)
def edit_blog_post(request, id):
post = Post.query.get(id)
form = ArticleEdit(obj=post)
# Since we didn't provide a query_factory for the 'blog' field, we need
# to set a dynamic one in the view.
form.blog.query = Blog.query.filter(Blog.author == request.user) \
.order_by(Blog.name)
Let's suppose we have a model, Foo, that references another model, User - and there are Flask-Admin's ModelView for both.
On the Foo admin view page
I would like the entries in the User column to be linked to the corresponding User model view.
Do I need to modify one of Flask-Admin's templates to achieve this?
(This is possible in the Django admin interface by simply outputting HTML for a given field and setting allow_tags (ref) True to bypass Django's HTML tag filter)
Some example code based on Joes' answer:
class MyFooView(ModelView):
def _user_formatter(view, context, model, name):
return Markup(
u"<a href='%s'>%s</a>" % (
url_for('user.edit_view', id=model.user.id),
model.user
)
) if model.user else u""
column_formatters = {
'user': _user_formatter
}
Use column_formatters for this: https://flask-admin.readthedocs.org/en/latest/api/mod_model/#flask.ext.admin.model.BaseModelView.column_formatters
Idea is pretty simple: for a field that you want to display as hyperlink, either generate a HTML string and wrap it with Jinja2 Markup class (so it won't be escaped in templates) or use macro helper: https://github.com/mrjoes/flask-admin/blob/master/flask_admin/model/template.py
Macro helper allows you to use custom Jinja2 macros in overridden template, which moves presentational logic to templates.
As far as URL is concerned, all you need is to find endpoint name generated (or provided) for the User model and do url_for('userview.edit_view', id=model.id) to generate the link.
extra information for #wodow, notice that model.user is wrong if you use pymongo as the backend, because the model in pymongo is a dict type, you can just use model['name'] to replace it
Adding this code to each of your models that have referenced by other models and flask-admin and jinja will take care of the name you want to display on the screen, just replace that with whatever you prefer:
def __unicode__(self):
return self.name # or self.id or whatever you prefer
for example:
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 MasterUser(db.Document, UserMixin):
email = db.StringField(max_length=255)
password = db.StringField(max_length=255)
active = db.BooleanField(default=True)
confirmed_at = db.DateTimeField()
roles = db.ListField(db.ReferenceField(Role), default=[])