I have the following database table:
import sqlalchemy as sql
class Users(db.Model):
__tablename__ = "users"
id = sql.Column(sql.Integer, primary_key=True)
username = sql.Column(sql.String,index=True, unique=True, nullable=False)
email = sql.Column(sql.String(120),index=True, unique=True, nullable=False)
last_seen = sql.Column(sql.DateTime, default=datetime.utcnow())
password = sql.Column(sql.String(128), nullable=False)
and I have the following schema:
class UserSchema(ma.SQLAlchemySchema):
class Meta:
model = Users
ordered = True
id = ma.auto_field(dump_only=True)
username = ma.auto_field(required=True,validate=validate.Length(min=3, max=64))
email = ma.auto_field(required=True, validate=[validate.Length(max=128),validate.Email()])
password = ma.auto_field(required=True, load_only=True,validate=validate.Length(min=5))
so I am trying to use pytest with flask to test if I can add a user and I generally get the error:
tests/test_users.py::test_create_user {"messages":{"json":{"email":["Missing data for required field."],"password_hash":["Missing data for required field."],"username":["Missing data for required field."]}}}
The function that I am using is:
def test_create_user():
username = "testing"
password = "12345678"
email = "testing#mail.io"
flask_app = create_app()
with flask_app.test_client() as test_client:
response = test_client.post("/api/users", data=json.dumps({
'username':username,
'password':password,
'email':email
}))
print(response.data.decode('utf-8'))
assert response.status == 201
assert b'id' in response.data
assert b'password' not in response.data
assert b'username' in response.data
I am doing this with postman and I am able to get the correct expected response. So How can I do this with pytest and flask?. I want to be get the expected responses with pytest.
Follow the instruction here.
In short:
Replace data= with json=
Don't create a json-string with json.dumps(), but pass in the dictionary.
The test client will create the json from it and pass it to your Flask code in the desired manner.
Related
I am new to python flask. I am creating an API for user registration where a user may have multiple languages. so I am referencing list of languages in my user object.
please get my code here..
class User(Document):
name = StringField(required=True)
email = EmailField(required=True, primary_key=True)
languages = ListField(ReferenceField(Language), required=True)
class Language(Document):
name = StringField(required=True, unique=True)
active = BooleanField(default=True)
#app.route("/register", methods=["POST"])
def register_user():
request_payload = request.json
user = User(
name = request_payload["name"],
email = request_payload["email"]
)
for lang in request_payload["languages"]:
user.languages.append(Language.objects(name=lang))
user.save()
**Post request Payload
======================**
{
"name":"xxxx",
"email":"xxx#gmail.com",
"languages":["English","Marathi"]
}
**Error:
========**
ValidationError (User:xxx#gmail.com) (A ReferenceField only accepts DBRef, LazyReference, ObjectId or documents: ['languages'])
Document object returns QuerySet ..it was causing that error. ..for loop changed and it worked
for lang in request_payload["languages"]:
obj = Languages.objects(name=lang)
# print("******" + str(obj.count()), file=sys.stdout)
if obj.count() == 0:
return error_return("Incorrect Language: "+lang+ " is not present in Languages collection", 400)
for l1 in obj:
user.languages.append(l1)
I've created a form which takes user's name and their email address. I get this data from the form and put it into a sqlite3 database in the following way:
#app.route('/my_form', methods=["GET", "POST"])
def form_data():
if request.method == "POST":
user_name = request.form["name"]
new_user = form_database(name=user_name)
user_email = request.form["email"]
new_user_email = form_database(email=user_email)
try:
db.session.add(new_user)
db.session.add(new_user_email)
db.session.commit()
return redirect("/my_form")
Current result: each data entry gets recorded into a new row:
1|Jack||||||||||
2||svisto#hotmail.com|||||||||
Desirable result: each data entry gets recorded into the same row:
1|Jack|svisto#hotmail.com|||||||||
Question: How can I change the code such that I get the desirable result?
Lets say you have a User class in your model like this:
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer,
primary_key=True)
username = db.Column(db.String(32),
index=False,
unique=True,
nullable=False)
email = db.Column(db.String(64),
index=True,
unique=True,
nullable=False)
Then you can do this in your try block
try:
new_user = User(user_name=user_name,
email=email)
db.session.add(new_user)
db.session.commit()
Solution I found:
I fused:
new_user = form_database(name=user_name) and new_user_email = form_database(email=user_email)
together such that the code looks like this:
#app.route('/my_form', methods=["GET", "POST"])
def form_data():
if request.method == "POST":
user_name = request.form["name"]
user_email = request.form["email"]
new_user_details = form_database(name=user_name, email=user_email)#assigns 2 form inputs for both columns in the database model to the same variable
try:
db.session.add(new_user_details)#adds that variable to the database as one entry, hence in one row but different columns
db.session.commit()
I've been following along to Corey Schafer's awesome youtube tutorial on the basic flaskblog. In addition to Corey's code, I`d like to add a logic, where users have to verify their email-address before being able to login. I've figured to do this with the URLSafeTimedSerializer from itsdangerous, like suggested by prettyprinted here.
The whole token creation and verification process seems to work. Unfortunately due to my very fresh python knowledge in general, I can't figure out a clean way on my own how to get that saved into the sqlite3 db. In my models I've created a Boolean Column email_confirmed with default=False which I am intending to change to True after the verification process. My question is: how do I best identify the user (for whom to alter the email_confirmed Column) when he clicks on his custom url? Would it be a good practice to also save the token inside a db Column and then filter by that token to identify the user?
Here is some of the relevant code:
User Class in my modely.py
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
image_file = db.Column(db.String(20), nullable=False, default='default_profile.jpg')
password = db.Column(db.String(60), nullable=False)
date_registered = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
email_confirmed = db.Column(db.Boolean(), nullable=False, default=False)
email_confirm_date = db.Column(db.DateTime)
projects = db.relationship('Project', backref='author', lazy=True)
def get_mail_confirm_token(self, expires_sec=1800):
s = URLSafeTimedSerializer(current_app.config['SECRET_KEY'], expires_sec)
return s.dumps(self.email, salt='email-confirm')
#staticmethod
def verify_mail_confirm_token(token):
s = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
try:
return s.loads(token, salt='email-confirm', max_age=60)
except SignatureExpired:
return "PROBLEM"
Registration Logic in my routes (using a users blueprint):
#users.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('dash.dashboard'))
form = RegistrationForm()
if form.validate_on_submit():
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
user = User(username=form.username.data, email=form.email.data, password=hashed_password)
db.session.add(user)
db.session.commit()
send_mail_confirmation(user)
return redirect(url_for('users.welcome'))
return render_template('register.html', form=form)
#users.route('/welcome')
def welcome():
return render_template('welcome.html')
#users.route('/confirm_email/<token>')
def confirm_email(token):
user = User.verify_mail_confirm_token(token)
current_user.email_confirmed = True
current_user.email_confirm_date = datetime.utcnow
return user
The last parts current_user.email_confirmed = True and current_user.email_confirm_date =datetime.utcnow are probably the lines in question. Like stated above the desired entries aren't made because the user is not logged in at this stage, yet.
Im grateful for any help on this!
Thanks a lot in advance!
Thanks to #exhuma. Here is how I eventually got it to work - also in addition I'm posting the previously missing part of email-sending.
User Class in my models.py
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
image_file = db.Column(db.String(20), nullable=False, default="default_profile.jpg")
password = db.Column(db.String(60), nullable=False)
date_registered = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
email_confirmed = db.Column(db.Boolean(), nullable=False, default=False)
email_confirm_date = db.Column(db.DateTime)
projects = db.relationship("Project", backref="author", lazy=True)
def get_mail_confirm_token(self):
s = URLSafeTimedSerializer(
current_app.config["SECRET_KEY"], salt="email-comfirm"
)
return s.dumps(self.email, salt="email-confirm")
#staticmethod
def verify_mail_confirm_token(token):
try:
s = URLSafeTimedSerializer(
current_app.config["SECRET_KEY"], salt="email-confirm"
)
email = s.loads(token, salt="email-confirm", max_age=3600)
return email
except (SignatureExpired, BadSignature):
return None
Send Mail function in my utils.py
def send_mail_confirmation(user):
token = user.get_mail_confirm_token()
msg = Message(
"Please Confirm Your Email",
sender="noreply#demo.com",
recipients=[user.email],
)
msg.html = render_template("mail_welcome_confirm.html", token=token)
mail.send(msg)
Registration Logic in my routes.py (using a users blueprint):
#users.route("/register", methods=["GET", "POST"])
def register():
if current_user.is_authenticated:
return redirect(url_for("dash.dashboard"))
form = RegistrationForm()
if form.validate_on_submit():
hashed_password = bcrypt.generate_password_hash(form.password.data).decode(
"utf-8"
)
user = User(
username=form.username.data, email=form.email.data, password=hashed_password
)
db.session.add(user)
db.session.commit()
send_mail_confirmation(user)
return redirect(url_for("users.welcome"))
return render_template("register.html", form=form)
#users.route("/welcome")
def welcome():
return render_template("welcome.html")
#users.route("/confirm_email/<token>")
def confirm_email(token):
email = User.verify_mail_confirm_token(token)
if email:
user = db.session.query(User).filter(User.email == email).one_or_none()
user.email_confirmed = True
user.email_confirm_date = datetime.utcnow()
db.session.add(user)
db.session.commit()
return redirect(url_for("users.login"))
flash(
f"Your email has been verified and you can now login to your account",
"success",
)
else:
return render_template("errors/token_invalid.html")
Only missing from my point of view is a simple conditional logic, to check if email_confirmed = True before logging in, as well as the same check inside the confirm_email(token) function to not make this process repeatable in case the user clicks on the confirmation link several times. Thanks again! Hope this is of some help to anyone else!
The key to your question is this:
My question is: how do I best identify the user (for whom to alter the email_confirmed Column) when he clicks on his custom url?
The answer can be seen in the example on URL safe serialisation using itsdangerous.
The token itself contains the e-mail address, because that's what you are using inside your get_mail_confirm_token() function.
You can then use the serialiser to retrieve the e-mail address from that token. You can do that inside your verify_mail_confirm_token() function, but, because it's a static-method you still need a session. You can pass this in as a separate argument though without problem. You also should treat the BadSignature exception from itsdangerous. It would then become:
#staticmethod
def verify_mail_confirm_token(session, token):
s = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
try:
email = s.loads(token, salt='email-confirm', max_age=60)
except (BadSignature, SignatureExpired):
return "PROBLEM"
user = session.query(User).filter(User.email == email).one_or_none()
return user
Would it be a good practice to also save the token inside a db Column and then filter by that token to identify the user?
No. The token should be short-lived and should not be kept around.
Finally, in your get_mail_confirm_token implementation you are not using the URLSafeTimedSerializer class correctly. You pass in a second argument called expires_sec, but if you look at the docs you will see that the second argument is the salt, which might lead to unintended problems.
I want to authomaticaly put name to authors blogged from loged in session.
So far I can log user but when he is already logged I cant find way to work with his name on site.
So I am trying to create some way, which will store username after he is logged in memory and flask will then use this username for blog posts and comments or editing profile. Thank you
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
username = Column(String(64))
password = Column(String(120))
email = Column(String(64))
def __init__(self, username, password, email):
self.username = username
self.password = password
self.email = email
Base.metadata.create_all(engine)
Base2 = declarative_base()
class Blogpost(Base2):
__tablename__ = 'blogpost'
id = Column(Integer, primary_key=True)
title = Column(String(50))
subtitle = Column(String(50))
author = Column(String(20))
date_posted = Column(DateTime)
content = Column(Text)
def __init__(self, title, subtitle, author, date_posted, content):
self.title = title
self.subtitle = subtitle
self.author = author
self.date_posted = date_posted
self.content = content
#app.route('/login', methods=['POST'])
def login():
POST_USERNAME = str(request.form['username'])
POST_PASSWORD = str(request.form['password'])
def check_password(hashed_password, user_password):
password, salt = hashed_password.split(':')
return password == hashlib.sha256(salt.encode() + user_password.encode()).hexdigest()
Session = sessionmaker(bind=engine)
s = Session()
user = s.query(User).filter_by(username=POST_USERNAME).first()
if check_password(user.password, POST_PASSWORD) == True:
session['logged_in'] = True
user_name = POST_USERNAME
else:
flash('wrong password!')
return index()
#app.route('/add')
def add():
return render_template('add.html')
#app.route('/addpost', methods=['POST'])
def addpost():
title = request.form['title']
subtitle = request.form['subtitle']
content = request.form['content']
Session = sessionmaker(bind=engine)
session = Session()
post = Blogpost(title=title, subtitle=subtitle, author=user_name, content=content, date_posted=datetime.now())
session.add(post)
session.commit()
I would encourage you to use an extension like flask-login for user management or flask-security for extended features, meanwhile, you can store the user in flask sessions.
first import session (i will call it login_session to differentiate it with your sql-alchemy session)
from flask import session as login_session
Then once a user logs in you can store the user details like this
login_session['username'] = user.username #user here being the user object you have queried
And to access the user name from session
username = login_session['username']
and once a user logs out, you delete the user details from session like this
del login_session['username']
But as others have mentioned in the comments, for a serious web app, you will want to consider using one of the flask extensions for user management
I use bcrypt for the authentication of my resources and have accounts stored in mydatabase with username and passwords. I have stored the passwords manually as hashes in the database as follows:
I started the python bash and typed in following code:
import bcrypt
password = u'passwordtobehashed'
password_hashed = bcrypt.hashpw(password, bcrypt.gensalt())
print (password_hashed)
Then i copied the output of print and stored it in the account table via a POST Request(w/o authentication):
curl -d '{"username": "someuser", "password": "somehashedpassword", "roles_id": 1}' -H 'Content-Type: application/json' http://127.0.0.1:5000/account
Well I use SQLAlchemy and eve is also up to date (Version: 0.7.1).
Well I request for example the people resource using Bcrypted Authentication as follows:
curl -u username 127.0.0.1:5000/people
Then I enter my password and I get following error:
File "/home/vagrant/erpghost/restapi/oldtrivial.py", line 57, in check_auth
accounts = app.data.driver.db['account']
AttributeError: 'SQLAlchemy' object has no attribute 'db'
For some reason the db attribute is not available. I also tried to use Eve.app.data.driver.db and I tried importing current_app from flask but that all did not work.
Well here is my code:
oldtrivial.py
from eve.auth import BasicAuth
from eve import Eve
from eve_sqlalchemy import SQL
from eve_sqlalchemy.validation import ValidatorSQL
import bcrypt
from connection import connect
from connection import Base
con, meta = connect()
Base.metadata.create_all(con)
class BCryptAuth(BasicAuth):
def check_auth(self, username, password, allowed_roles, resource, method):
accounts = app.data.driver.db['account']
account = accounts.find_one({'username': username})
return account and \
bcrypt.hashpw(password, account['password']) == account['password']
app = Eve(validator=ValidatorSQL, data=SQL, auth=BCryptAuth)
db = app.data.driver
Base.metadata.bind = db.engine
db.Model = Base
db.create_all()
if __name__ == '__main__':
app.run(debug=True, use_reloader=False)
tables.py
from sqlalchemy.orm import column_property
from sqlalchemy import Column, Integer, String, DateTime, func, ForeignKey
from connection import connect
from eve.auth import BasicAuth
from connection import Base
from sqlalchemy.orm import relationship
con, meta = connect()
class CommonColumns(Base):
__abstract__ = True
_created = Column(DateTime, default=func.now())
_updated = Column(DateTime, default=func.now(), onupdate=func.now())
_etag = Column(String(40))
class People(CommonColumns):
__tablename__ = 'people'
_id = Column(Integer, primary_key=True, autoincrement=True)
firstname = Column(String(80))
lastname = Column(String(120))
fullname = column_property(firstname + " " + lastname)
class Roles(CommonColumns):
__tablename__ = 'roles'
_id = Column(Integer, primary_key=True, autoincrement=True)
role = Column(String(80))
class Account(CommonColumns):
__tablename__ = 'account'
_id = Column(Integer, primary_key=True, autoincrement=True)
username = Column(String(50), nullable=False, unique=True)
password = Column(String(200), nullable=False)
roles = relationship("Roles", backref="account")
roles_id = Column(Integer, ForeignKey('roles._id'))
settings.py
from eve_sqlalchemy.decorators import registerSchema
from eve.utils import config
from tables import People
from tables import Account
from tables import Roles
registerSchema('people')(People)
registerSchema('roles')(Roles)
registerSchema('account')(Account)
DOMAIN = {
'people': People._eve_schema['people'],
'roles': Roles._eve_schema['roles'],
'account': Account._eve_schema['account'],
}
DOMAIN['account'].update({
'additional_lookup': {
'url': 'regex("[\w]+")',
'field': 'username'
},
'cache_control': '',
'cache_expires': 0,
'allowed_roles': ['superuser', 'admin'],
'authentication': None,
})
SQLALCHEMY_DATABASE_URI = 'postgresql://databaseuser:password#localhost:5432/database'
RESOURCE_METHODS = ['GET', 'POST']
ITEM_METHODS = ['GET', 'DELETE', 'PATCH', 'PUT']
DEBUG = True
config.ID_FIELD = config.ITEM_LOOKUP_FIELD = '_id'
DOMAIN['people']['id_field'] = config.ID_FIELD
DOMAIN['roles']['id_field'] = config.ID_FIELD
DOMAIN['account']['id_field'] = config.ID_FIELD
Hope someone can help me out.
Something along these lines should work:
from flask import current_app
from tables import Account
# ...
def check_auth(...):
session = current_app.data.driver.session
return session.query(Account) \
.filter(Account.username == username,
Account.password == hashed_password) \
.count() > 0
I guess you've tried to mimic the code in http://python-eve.org/authentication.html#basic-authentication-with-bcrypt ? This is for using Mongo-DB instead of SQLAlchemy.