SQLAlchemy: How do I find second degree relationships in the database? - python

I'm writing a permission system for my Flask app, and I'm having problems figuring out how to find relationships in the database. My github repo, if you want to see everything. The decorator is intended to limit access to decorated views.
def user_has(attribute):
"""
Takes an attribute (a string name of either a role or an ability) and returns the function if the user has that attribute
"""
def wrapper(func):
#wraps(func)
def inner(*args, **kwargs):
attribute_object = Role.query.filter_by(name=attribute).first() or \
Ability.query.filter_by(name=attribute).first()
if attribute_object in current_user.roles or attribute in current_user.roles.abilities.all():
return func(*args, **kwargs)
else:
# Make this do someting way better.
return "You do not have access"
return inner
return wrapper
I'm using SQLAlchemy and storing Users, Roles, and Abilities in the database. Users may have one or more roles. Roles may have one or more abilities. I want to take the string passed to the decorator and check if the user has that role or if one of the user's roles has that ability. The decorator shouldn't care whether it has been called with a role or ability argument.
Apparently, this method (current_user.roles.abilities.all()) does not work to get through my relational database as I'm attempting to do here to find abilities. I get an error message:
AttributeError: 'InstrumentedList' object has no attribute 'abilities'
How can I compare the string argument with my current user's abilities which are derived from his/her roles?
For reference, my models:
user_role_table = db.Table('user_role',
db.Column(
'user_id', db.Integer, db.ForeignKey('user.uid')),
db.Column(
'role_id', db.Integer, db.ForeignKey('role.id'))
)
role_ability_table = db.Table('role_ability',
db.Column(
'role_id', db.Integer, db.ForeignKey('role.id')),
db.Column(
'ability_id', db.Integer, db.ForeignKey('ability.id'))
)
class Role(db.Model):
__tablename__ = 'role'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), unique=True)
abilities = db.relationship(
'Ability', secondary=role_ability_table, backref='roles')
def __init__(self, name):
self.name = name.lower()
def __repr__(self):
return '<Role {}>'.format(self.name)
def __str__(self):
return self.name
class Ability(db.Model):
__tablename__ = 'ability'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), unique=True)
def __init__(self, name):
self.name = name.lower()
def __repr__(self):
return '<Ability {}>'.format(self.name)
def __str__(self):
return self.name
class User(db.Model):
__tablename__ = 'user'
uid = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True)
pwdhash = db.Column(db.String(100))
roles = db.relationship('Role', secondary=user_role_table, backref='users')
def __init__(self, email, password, roles=None):
self.email = email.lower()
# If only a string is passed for roles, convert it to a list containing
# that string
if roles and isinstance(roles, basestring):
roles = [roles]
# If a sequence is passed for roles (or if roles has been converted to
# a sequence), fetch the corresponding database objects and make a list
# of those.
if roles and is_sequence(roles):
role_list = []
for role in roles:
role_list.appen(Role.query.filter_by(name=role).first())
self.roles = role_list
# Otherwise, assign the default 'user' role. Create that role if it
# doesn't exist.
else:
r = Role.query.filter_by(name='user').first()
if not r:
r = Role('user')
db.session.add(r)
db.session.commit()
self.roles = [r]
self.set_password(password)
def set_password(self, password):
self.pwdhash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.pwdhash, password)
def is_authenticated(self):
return True
def is_active(self):
return True
def is_anonymous(self):
return False
def get_id(self):
return unicode(self.uid)
def __repr__(self):
return '<User {}>'.format(self.email)
def __str__(self):
return self.email
and the decorated view:
#app.route('/admin', methods=['GET', 'POST'])
#user_has('admin')
def admin():
users = models.User.query.all()
forms = {user.uid: RoleForm(uid=user.uid, roles=[role.id for role in user.roles])
for user in users}
if request.method == "POST":
current_form = forms[int(request.form['uid'])]
if current_form.validate():
u = models.User.query.get(current_form.uid.data)
u.roles = [models.Role.query.get(role)
for role in current_form.roles.data]
db.session.commit()
flash('Roles updated for {}'.format(u))
return render_template('admin.html', users=users, forms=forms)

The solution ended up being dead simple. I feel a bit silly not knowing it from the start.
user_abilities = []
for role in current_user.roles:
user_abilities += [role.ability for ability in role.abilities]
I still feel there's probably a better pattern for this, but the solution works without a hitch.

Any chance this doesn't work because you use attribute instead of attribute_object in the second clause of your if statement?
Instead of this:
if attribute_object in current_user.roles or attribute in current_user.roles.abilities.all():
Try this:
if attribute_object in current_user.roles or attribute_object in current_user.roles.abilities.all():

Related

Flask-Security: How to get / list / print all roles for a given user

I want to list all roles a given user has.
I'm not looking for current_user nor has_role.
The idea is to make an 'edituser.html' where an admin can change/add/remove roles for a given user. For that use case I need to show what roles the user to be edited has.
I've read: Flask Security- check what Roles a User has but I don't understand how to use it in for example a route/view.
My models.py is like this.
class Role(db.Document, RoleMixin):
def __str__(self):
return self.name
name = db.StringField(max_length=80, unique=True)
description = db.StringField(max_length=255)
permissions = db.StringField(max_length=255)
class User(db.Document, UserMixin):
def __str__(self):
return self.username
username = db.StringField(max_length=255)
password = db.StringField(max_length=255)
active = db.BooleanField(default=True)
fs_uniquifier = db.StringField(max_length=64, unique=True)
confirmed_at = db.DateTimeField()
current_login_at = db.DateTimeField()
last_login_at = db.DateTimeField()
current_login_ip = db.StringField(max_length=255)
last_login_ip = db.StringField(max_length=255)
login_count = db.IntField(max_length=255)
roles = db.ListField(db.ReferenceField(Role), default=[])
user_datastore = MongoEngineUserDatastore(db, User, Role)
Here is something I do in my app:
#staticmethod
def get_users():
"""Return list of all users"""
attrs = (
"email",
"active",
"confirmed_at",
"create_datetime",
"update_datetime",
"last_login_at",
"current_login_at",
"current_login_ip",
)
query = current_app.sywuserdb.user_model.query
users = query.all()
# convert to a list of dict of interesting info
rv = []
for user in users:
# copy simple attributes
userdata = {}
for attr in attrs:
userdata[attr] = getattr(user, attr)
userdata["roles"] = [r.name for r in user.roles]
rv.append(userdata)
# users is a list of tuples - convert to list of dict
return {"status": 200, "msgs": [], "data": rv}
Change 'sywuserdb' to your data store ('user_datastore' in your question).
This is called as part of your API - I have an 'admin' blueprint that has the following endpoint defined:
#api.route("/users", methods=["GET"])
#auth_required("token", "session")
#roles_accepted("admin")
def get_users():
rv = get_users()
return flask.jsonify(rv), rv["status"]
Ignore the #staticmethod - that is since I have it as part of a UserFactory class since I have a bunch of admin methods and API to manage users.

Sql Alchemy: Child object doesn't load during before_commit event listener

I am working on configuring my app to automatically update an elasticsearch (es) index anytime specified database tables/columns are updated. I'm basing it off Michael Grinberg's Flask Megatutorial. I am using a SQLAlchemy session event listeners (before_commit and after_commit) to collect the changes and determine when to update the es index.
The table I am indexing is a user table that has an address table as its child. Besides indexing the user's name, I also want to index their city, which comes from the address table. My issue is that address field populates as None, instead of loading the address. When creating a new user (along with its address), I receive the following error: 'This session is in 'committed' state; no further SQL can be emitted within this transaction.' when I try to index the city value.
I have also tried using the after_flush_postexec event, with the same results. Do you have any suggestions for configurations to load the child relationship in either the 'after_commit' or the 'after_flush_postexec' events?
models.py
class SearchableMixin(object):
#classmethod
def before_commit(cls, session):
session._changes = {
'add': list(session.new),
'update': list(session.dirty),
'delete': list(session.deleted)
}
#classmethod
def after_commit(cls, session):
for obj in session._changes['add']:
if isinstance(obj, SearchableMixin):
#do stuff to add User name and city to index
for obj in session._changes['update']:
if isinstance(obj, SearchableMixin):
# do stuff to update User name and city in index
for obj in session._changes['delete']:
if isinstance(obj, SearchableMixin):
# do stuff to delete User name and city from index
session._changes = None
class User(SearchableMixin, db.model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
address = db.relationship("Address", backref="user", lazy='joined', uselist=False)
#hybrid_property
def city(self):
return self.address.city
class Address(db.Model):
id = db.Column(db.Integer, primary_key=True)
city = db.Column(db.String(50))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
Here's the solution I came up with. While solving it, it presented another issue for which I've asked a [separate question][1]. However, while far from perfect, it does seem to work.
Instead of using sqlalchemy event hooks, I first subclassed db.model, adding in a CRUD mixin that adds create, update, save, delete functions. Then in the Searchable Mixin, I overrode those create, update, and delete functions to call the appropriate indexing function.
models.py
class CRUDMixin(object):
#classmethod
def create(cls, **kwargs):
"""Create a new record and save it the database."""
instance = cls(**kwargs)
saved = instance.save()
return saved
def update(self, commit=True, **kwargs):
for attr, value in kwargs.items():
if value != getattr(self, attr):
setattr(self, attr, value)
return commit and self.save() or self
def save(self, commit=True):
db.session.add(self)
if commit:
try:
db.session.commit()
except Exception:
db.session.rollback()
raise
return self
def delete(self, commit=True):
"""Remove the record from the database."""
db.session.delete(self)
return commit and db.session.commit()
class Model(CRUDMixin, db.Model):
__abstract__ = True
class SearchableMixin(object):
#classmethod
def create(cls, **kwargs):
try:
new = super().create(**kwargs)
add_to_index(new)
db.session.commit()
return new
except Exception:
raise
def update(self, commit=True, **kwargs):
try:
super().update(commit, **kwargs)
add_to_index(self)
db.session.commit()
return self
except Exception:
raise
def delete(self, commit=True):
try:
super().delete(commit)
remove_from_index(self)
db.session.commit()
return None
except Exception:
raise
class User(SearchableMixin, Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
address = db.relationship("Address", backref="user", lazy='joined', uselist=False)
#hybrid_property
def city(self):
return self.address.city
class Address(Model):
id = db.Column(db.Integer, primary_key=True)
city = db.Column(db.String(50))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
[1]: https://stackoverflow.com/questions/62722027/sql-alchemy-wont-drop-tables-at-end-of-test-due-to-metadata-lock-on-db

FLASK After commit causes odd IntegrityError violates unique constraint

UPDATE / CLARIFICATION
I confirmed that this strange behavior only occurs on the macOS machine, moving everything to a windows machine (using sqlite and doing a fresh init and migrate) doesn't cause the error... doing the same on my High Sierra box does cause the odd error.
Is anyone familiar with some known difference between sqlalchemy on Windows and macOS that might help?
Short version... I'm getting an integrity error (unique constraint) after I try to commit ANY entry to the DB, even if there are NO EXISTING entries at all in the table... why?
DETAILS
I've built a FLASK project (roughly based on the Miguel Grinberg Flask Maga Tutorial) using postgresql and sqlalchemy, the front-end has a page to register a user with a confirmation email (which works fine)... to save time I've written a route (see below) which pre-loads a confirmed user to the Users database, this user is the ONLY user in the Users table and I only visit the route ONE TIME.
After a successful commit I get an IntegrityError "duplicate key value violates unique constraint". This route only adds ONE user to an existing EMPTY Users table. The data IS successfully saved to the DB, the user can log in, but an error gets thrown. I get a similar error (see below) but am focusing on this route as an example because it is shorter than other views I've written.
EXAMPLE OF ROUTE CAUSING UNIQUE CONSTRAINT ERROR
#main.route('/popme')
##login_required
def popme():
## add user
u1 = User()
u1.email = 'user#domain.com'
u1.username = 'someuser'
u1.password_hash = 'REMOVED'
u1.confirmed = '1'
u1.role_id = 3
u1.name = 'Some User'
db.session.add(u1)
db.session.commit()
flash('User someuser can now login!')
return redirect(url_for('main.index'))
I only started getting this error after moving the entire project from a Windows machine to a MacOS machine. I'm running Python 3.6 in a virtual environment, this error occurs if I'm using sqlite3 or postgresql.
I've written a much longer route which pre-fills in about 20 other tables successfully (does on commit() at the end, all data IS stored in the DB), however I get an IntegrityError "duplicate key value violates unique constraint" every time for a seemingly random entry. I've destroyed the DB, done an init, migrated... each time when the commit() is called a IntegrityError is thrown, each time on a different table, there is no apparent reasoning.
BELOW IS USER MODEL
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64), unique=True, index=True)
username = db.Column(db.String(64), unique=True, index=True)
password_hash = db.Column(db.String(128))
confirmed = db.Column(db.Boolean, default=False)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
name = db.Column(db.String(64))
last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
def ping(self):
self.last_seen = datetime.utcnow()
db.session.add(self)
#property
def password(self):
raise AttributeError('password is not a readable attribute')
#password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
def generate_confirmation_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'confirm': self.id})
def confirm(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('confirm') != self.id:
return False
self.confirmed = True
db.session.add(self)
return True
def generate_reset_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'reset': self.id})
def reset_password(self, token, new_password):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('reset') != self.id:
return False
self.password = new_password
db.session.add(self)
return True
def generate_email_change_token(self, new_email, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'change_email': self.id, 'new_email': new_email})
def change_email(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('change_email') != self.id:
return False
new_email = data.get('new_email')
if new_email is None:
return False
if self.query.filter_by(email=new_email).first() is not None:
return False
self.email = new_email
db.session.add(self)
return True
def can(self, permissions):
return self.role is not None and (self.role.permissions & permissions) == permissions
def is_administrator(self):
return self.can(Permission.ADMINISTRATOR)
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
if self.role is None:
if self.email == current_app.config['FLASKY_ADMIN']:
self.role = Role.query.filter_by(permissions=0xff).first()
if self.role is None:
self.role = Role.query.filter_by(default=True).first()
def __repr__(self):
return '<User %r>' % self.username
I've tried Sql-alchemy Integrity error but its my understanding that sqlalchemy does auto-increment primary keys.
UPDATED INTEGRITY ERROR
sqlalchemy.exc.IntegrityError: (psycopg2.IntegrityError) duplicate key value violates unique constraint "ix_users_email"
DETAIL: Key (email)=(worldbmd#gmail.com) already exists.
[SQL: 'INSERT INTO users (email, username, password_hash, confirmed, role_id, name, last_seen) VALUES (%(email)s, %(username)s, %(password_hash)s, %(confirmed)s, %(role_id)s, %(name)s, %(last_seen)s) RETURNING users.id'] [parameters: {'email': 'user#domain.com', 'username': 'someuser', 'password_hash': 'REMOVED', 'confirmed': '1', 'role_id': 1, 'name': 'Some User', 'last_seen': datetime.datetime(2018, 7, 16, 17, 27, 13, 451593)}]
I also got into the same problem..as Joost said flask app runs twice. So we need to set it to run only once. We can achieve it by adding use_reloader=False like:
if __name__ = "__main__":
app.run(debug=True, use_reloader=False)
or
we can directly set debug=False,
if __name__ = "__main__":
app.run(debug=False)
The integrity error is caused by trying to add a blister to the blisters table with a non unique property. I think your model looks something like this:
class Blister(db.Model):
__tablename__ = 'blisters'
id = db.Column(db.Integer, primary_key=True)
name= db.Column(db.String(64), unique=True)
notes= db.Column(db.String(64))
cost= db.Column(db.Float)
And you're trying to add a blister with a name Small round dome which is already in the blisters table in the database, therefore causing the Integrity Error.

Using flask-login with flask-admin

I'm looking at an open source app https://github.com/AlvinCJin/deepfit-app/tree/master/app
This is using flask-admin for admin purposes/ On the page https://github.com/AlvinCJin/deepfit-app/blob/master/app/main/views.py:
# admin management setup
admin.add_view(ModelView(User, db.session))
admin.add_view(ModelView(Post, db.session))
path = op.join(os.path.abspath(__file__ + "/../../"), 'static') # need to get parent path of this code
admin.add_view(FileAdmin(path, '/static/', name='Static Files'))
This works fine as far as producing a helpful dashboard but is unsecured. I have flask-login installed and I read the section http://flask-admin.readthedocs.io/en/latest/introduction/#rolling-your-own , but it is unclear to me how to link the class discussed:
class MicroBlogModelView(sqla.ModelView):
def is_accessible(self):
return login.current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access
return redirect(url_for('login', next=request.url))
with the admin route.
The user table is defined in models.py as:
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
nickname = db.Column(db.String(64), unique=True)
firstname = db.Column(db.String(100))
lastname = db.Column(db.String(100))
email = db.Column(db.String(120), index=True, unique=True)
pwdhash = db.Column(db.String(54))
phone = db.Column(db.Integer)
address = db.Column(db.String(64))
confirmed = db.Column(db.Boolean, default=False)
role = db.Column(db.SmallInteger, default=ROLE_APPLICANT)
comments = db.relationship('Comment', backref='author', lazy='dynamic')
posts = db.relationship('Post', order_by="Post.timestamp", backref='author',
lazy='dynamic', cascade="all, delete, delete-orphan")
about_me = db.Column(db.Text())
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
member_since = db.Column(db.DateTime(), default=datetime.utcnow)
portrait = db.Column(db.String(140))
pref = db.relationship('Preference', uselist=False, backref='author')
fav = db.relationship('Favourite', backref='user', lazy='dynamic')
active = db.Column(db.Boolean, default=False)
#staticmethod
def make_unique_nickname(nickname):
if User.query.filter_by(nickname=nickname).first() is None:
return nickname
version = 2
while True:
new_nickname = nickname + str(version)
if User.query.filter_by(nickname=new_nickname).first() is None:
break
version += 1
return new_nickname
def __init__(self, nickname, firstname, lastname, email, password, role):
self.nickname = nickname.title()
self.firstname = firstname.title()
self.lastname = lastname.title()
self.email = email.lower()
self.set_password(password)
self.role = role
def ping(self):
self.last_seen = datetime.utcnow()
db.session.add(self)
def set_password(self, password):
self.pwdhash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.pwdhash, password)
def is_authenticated(self):
return True
def generate_confirmation_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'confirm': self.id})
def confirm(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('confirm') != self.id:
return False
self.confirmed = True
db.confirmed = True
db.session.add(self)
return True
def to_json(self):
json_user = {
'url': url_for('api.get_post', id=self.id, _external=True),
'nickname': self.nickname,
'member_since': self.member_since,
'last_seen': self.last_seen,
'posts': url_for('api.get_user_posts', id=self.id, _external=True),
'post_count': self.posts.count(),
}
return json_user
def generate_reset_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'reset': self.id})
def generate_auth_token(self, expiration):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'id': self.id})
#staticmethod
def verify_auth_token(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return None
return User.query.get(data['id'])
def is_active(self):
if self.active is True:
return True
else:
return False
def is_anonymous(self):
return False
def get_id(self):
return unicode(self.id)
def __repr__(self):
return '<User %r>' % self.nickname
And has a is_authenticated method, but how do I use this to require login of as specific user?
I have tried :
class MyView(BaseView):
#expose('/')
def index(self):
return self.render('admin/index.html')
def is_accessible(self):
return login.current_user.is_authenticated()
# admin management setup
admin.add_view(ModelView(User, db.session))
admin.add_view(ModelView(Post, db.session))
path = op.join(os.path.abspath(__file__ + "/../../"), 'static') # need to get parent path of this code
admin.add_view(FileAdmin(path, '/static/', name='Static Files'))
Based on:
https://flask-admin.readthedocs.io/en/v1.0.7/quickstart/
EDIT:
So just for my understanding you are subclassing the ModelViews and adding the ability to have routes?
I've changed it to:
class MyView(ModelView):
#expose('/')
def index(self):
return self.render('admin/index.html')
def is_accessible(self):
return login.current_user.is_authenticated()
# admin management setup
admin.add_view(MyView(User, db.session))
admin.add_view(MyView(Post, db.session))
path = op.join(os.path.abspath(__file__ + "/../../"), 'static') # need to get parent path of this code
admin.add_view(FileAdmin(path, '/static/', name='Static Files'))
That is getting closer but I need to integrate this with flask login - I'm getting:
NameError: global name 'login' is not defined
EDIT2:
I've changed it to:
class MyView(ModelView):
#expose('/')
#login_required
def index(self):
return self.render('admin/index.html')
removing the is_accessible function, so its not overridden, The user class already has a built in is_accessible function.
This is at least partially working, but I would like to only allow access to admins that have a ROLE in user defined as 0
You need it do it like below
from flask_login.utils import current_user
class MyView(ModelView):
#expose('/')
def index(self):
return self.render('admin/index.html')
def is_accessible(self):
return current_user.is_authenticated()
# admin management setup
admin.add_view(MyView(User, db.session))
admin.add_view(MyView(Post, db.session))
path = op.join(os.path.abspath(__file__ + "/../../"), 'static') # need to get parent path of this code
admin.add_view(FileAdmin(path, '/static/', name='Static Files'))
You need to use MyView class while registering the views. If you need more customization then you need to implement them in your User object used by Flask login. There you can create check group and anything else you need

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.

Categories

Resources