Use service for queries in Flask application - python

I am working with flask and SQLAlchemy, so let's say for example i have a class User :
class User(UserMixin, Base):
__tablename__ = 'user'
id = Column(Integer, nullable=False, autoincrement=True, primary_key=True)
email = Column(String(100), nullable=False, unique=True)
password = Column(String(100))
name = Column(String(100))
Now in my views every time i make a query to get a user by id or by name i make a query.So i thought to make a class called userService and put all queries in it.So i could have :
class UserService():
def getUserByName(self, name):
return User.query.filter(User.name == name).first()
def getUserById(self, id):
return User.query.get(id)
def getUserByEmail(self, email):
return User.query.filter(User.email == email).first()
Now in my views i can call the UserService class every time i need a user.I use application factory, so my question is: Is it more efficient to instantiate the UserService object in my create_app function and then import it in my views.py file, or instantiate a new UserService object in every route in my views.py file?

It seems you only need UserService class for namespace so you don't need to create the instance. You can change your methods to staticmethods or create a module that contains these functions:
# userservice.py
class UserService():
#staticmethod
def getUserByName(name):
return User.query.filter(User.name == name).first()
# userservice.py
def getUserByName(name):
return User.query.filter(User.name == name).first()
...
I would prefer the second approach, its more clean and pythonic way of doing this:
# views.py
import userservice
user = userservice.getUserByName(name)
# first approach
from userservice import UserService
user = UserService.getUserByName(name)

Related

Trying to inherit from flask db.Model class -> __init__ not being called

In my flask application I'm trying to inherit from custom flask db.model class to add some simple logic on top of db.model object. Please find details below. I have two questions here:
When I instantiate my child-class Activity by calling Activity.query.first(), neither __init__ method of Activity class nor __init__ method of parent DBActivity class are being called. I would love to understand why they are not being called and how make them being called.
In general, is it a good/viable practice to inherit from flask model classes to implement business logic? If not - would would be a suggested practice?
Appreciate your help a lot!
in app.models I have:
...
class DBActivity(db.Model):
activity_id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
athlete_id = db.Column(db.Integer, db.ForeignKey('user.strava_id'), nullable=False)
json = db.Column(db.JSON)
laps = db.Column(db.JSON)
streams = db.Column(db.JSON)
df_json = db.Column(db.JSON)
intervals = db.Column(db.JSON)
blob = db.Column(db.BLOB)
comment = db.Column(db.Text)
def __init__(self):
print("DBActivity __init__")
I also have a following class inheriting from DBActivity:
class Activity(DBActivity):
def __init__(self):
super().__init__()
print('just returned from super().__init__')
print('running Activity.__init__')
Here is how I instantiate Activity:
with app.app_context():
a = Activity.query.first()
>>>a
<Activity 5567599209>
>>>isinstance(a, Activity)
True
>>>isinstance(a, DBActivity)
True

How can I use flask_login with multiple user classes and different database entries?

If someone were to have a flask web app with several "types" of user, for example in a school setting there may be "teacher", "student" and "admin". Each user needs to have a way to log in to the app, but Flask-Login only provides one #login.user_loader. My question is similar to this question, about multiple user classes, but the difference is that each type of user does not have the same model structure as the others. For example, if one were to use flask-sqlalchemy and declared the following models:
class Teacher(db.Model, UserMixin):
id = db.Column(db.Integer,primary_key=True)
title = db.Column(db.String(32))
pwd_hash = db.Column(db.String(128))
first_name = db.Column(db.String(128))
last_name = db.Column(db.String(128))
students = db.relationship('Student',backref='teacher',lazy='dynamic') #because a teacher will teach multiple students
class Student(db.Model, UserMixin):
id = db.Column(db.Integer,primary_key=True)
username = db.Column(db.String(64),unique=True)
teacher_id = db.Column(db.ForeignKey('teacher.id'),db.Integer) #for the ease of things, the student is only ever taught by one teacher
class Admin(db.Model, UserMixin):
email = db.Column(db.String(128),unique=True)
name = db.Column(db.String(128))
The user_loader is normally used like this, with the User class here being a generic user class:
#login.user_loader
def load_user(id):
return User.query.get(int(id))
This means that you have to have just one User class that can be logged in. The normal approach would be to condense the Student, Teacher and Admin class into one User class, with a role field in the model which tells you what level of access they are. But this brings up multiple problems. For example, not all students have an email at their disposal yet the administrative account for the school needs one. Similarly, the relationship between students and teachers would fail because there is no model for the teachers to have students in their class etc.
So how can I implement a way of there being multiple user models with different attributes and model fields that can each be logged in separately with flask_login?
Your User needs only an id and a relationship to the tables which that user corresponds to. You can create this by creating a table for roles and roles-relationships:
class User(db.Model, UserMixin):
""""""
__tablename__ = "user"
# Core
id = db.Column(db.Integer,primary_key=True)
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
class Role(db.Model, RoleMixin):
__tablename__ = "role"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True)
roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
You can check for a given role using the following:
class User(db.Model, UserMixin):
# all the args go here...
...
def has_role(self, role_name):
"""Does this user have this permission?"""
my_role = Role.query.filter_by(name=role_name).first()
if my_role in self.roles:
return True
else:
return False
Next, create a decorator which checks for a role
from flask import redirect
from flask_login import current_user
from functools import wraps
def require_role(role):
"""make sure user has this role"""
def decorator(func):
#wraps(func)
def wrapped_function(*args, **kwargs):
if not current_user.has_role(role):
return redirect("/")
else:
return func(*args, **kwargs)
return wrapped_function
return decorator
Step 3: Apply the decorator in operations:
#app.route("/")
#login_required
#require_role(role="teacher")
def teachers():
"""Only allow teachers"""
# Otherwise, proceed
return render_template('teachers.html')

Flask-Admin permissions for adding user with specific role

I use Flask-Admin and Flask-Security to implement admin panel for my Python application. Here are the models for User and for Role:
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
def __str__(self):
return self.name
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(255), unique=True, index=True)
password = db.Column(db.String(255))
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
def __str__(self):
return self.username
I created a sqla.ModelView for User available only for users with roles "admin" and "superadmin", so such users can create new users using this view. How can I make only users with role "superadmin" can create users with roles "superadmin" and "admin"? In other words I want to specify which of the variants are available for users with different roles: https://imgur.com/a/GdhUS2Y.
To do that you will have to customize a few things.
I would suggest the following:
add this method in your User model that checks if a user has a certain role:
def has_role(self, role):
# db is your database session.
query = db.query(Role).filter(Role.name == role).first()
if query:
if query.name in self.roles:
return True
return False
Now you have to customize the roles that are being displayed when a user tries to create a new user. To achieve that, you will need to change the definition of the roles field. To achieve that, you will need to override the flask-admin scaffold_form() method.
from flask_admin.contrib.sqla.fields import QuerySelectMultipleField
from flask_security import current_user
from flask_admin.form import Select2Widget
def scaffold_form(self):
form_class = super(YourUserAdminClass, self).scaffold_form()
role_filter = Role.name == Role.name if current_user.has_role('superadmin') else Role.name.notin_('superadmin', 'admin')
form_class.roles = QuerySelectMultipleField(
query_factory=lambda: self.session.query(Role).filter(role_filter),
allow_blank,
blank_text='Select Role',
widget=Select2Widget(multiple=False) # change this to True if you allow multiple roles
)
The code above will render the dropdown with the roles depending on the role that the current_user have (using the has_role() method in the filter
Although the answer by #shifloni provides a concise idea of what's need to be done, as per what I have observed, I think it's incomplete. It's because of the fact that
if current_user.has_role('superadmin') throws error during app initialization as current_user is available only when the code is executed in a request context. This is what worked for me:
class ViewForCreatingAdminAndOperators(sqla.ModelView):
def _run_view(self, fn, *args, **kwargs):
if current_user.has_role('admin'):
self.role_filter = Role.name.in_(['operator'])
else:
self.role_filter = Role.name.in_(['superadmin', 'admin', 'operator'])
return fn(self, *args, **kwargs)
def scaffold_form(self):
form = super(ViewForCreatingAdminUsers, self).scaffold_form()
form.roles = QuerySelectMultipleField(
query_factory=lambda: self.session.query(Role).filter(self.role_filter),
widget=Select2Widget(multiple=False) # change this to True if you allow multiple roles
)
return form
_run_view func is always called in a request context, so whenever we click to create a form, that func is called and we have the role_filter available.
P.S. Use case as per above code was that admin should be able to create only operator.

flask admin: How to make a query according to the current user's id and role?

I used the classes below, allowing users to edit their own profile after registering and it works well. I achieve this using get_query and get_count_query.
However, if the current user is administrator, how can I customize it to let him/her view all users' profiles instead of just his/her own profile? Thanks in advance.
from flask_admin.contrib.sqla.view import ModelView, func
class User(db.Model, UserMixin):
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(255), unique=True)
password = db.Column(db.String(255))
def __str__(self):
return self.email
class UserView(ModelView):
"""
Restrict only the current user can see his/her own profile
"""
def get_query(self):
return self.session.query(self.model).filter(self.model.id==current_user.id)
def get_count_query(self):
return self.session.query(func.count('*')).filter(self.model.id==current_user.id)
You can define another custom ModelView for administrator. For example:
class UserViewForAdmin(ModelView):
def is_accessible(self):
return current_user.has_role("admin")
def inaccessible_callback(self, name, **kwargs):
return redirect(url_for("security.login", next=request.url))
pass
admin = Admin(name="Flask-Admin Example")
admin.add_view(UserView(User, db.session, name="Profile")
admin.add_view(UserViewForAdmin(User, db.session, name="UserList", endpoint="users")
The is example assume you use Flask-Security to do user management.
Another option here is to check the user and handle accordingly in y our get_query or get_count_query such as:
def get_query(self):
if current_user.username == 'admin':
return self.session.query(self.model)
else:
return self.session.query(self.model).filter(
# your existing filter for user
)
Note: the above uses current_user from flask_security but can be adapted to other logic easily.

Flask-SQLAlchemy Constructor

in the Flask-SQLAlchemy tutorial, a constructor for the User model is defined:
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True)
email = db.Column(db.String(120), unique=True)
def __init__(self, username, email):
self.username = username
self.email = email
for a table with two columns, that might be acceptable, but what if I have tables with 10+ columns? do constructors have to be defined each time I define a new model?
In most cases not defining a constructor in your model class gives you the correct behavior.
Flask-SQLAlchemy's base model class (which is also SQLAlchemy's declarative base class) defines a constructor that just takes **kwargs and stores all the arguments given, so it isn't really necessary to define a constructor.
If you do need to define a constructor to do some model specific initialization, then do so as follows:
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True)
email = db.Column(db.String(120), unique=True)
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
# do custom initialization here
By letting the base class handle the **kwargs you free yourself from the complexity of initializing the fields of the model.
I know this is a little old but regardless it should be useful for someone else with similar issue.
If you encounter the "TypeError: init() takes exactly 1 argument (2 given)" - it means you need to provide the keywords when creating the objects to add to your database like so:
db.session.add(User(username='myname',email='my#email',password='mypassword')).
It is quite common to come across this small issue...but difficult to spot.
I hope this helps.
You can write the constructor however you want, you'll just need to initialize each field before trying to save the object in the database.
class User(db.Model):
...
user = User()
user.username = 'foo'
user.email = 'foo#bar.com'
db.session.add(user)
You can initialize parameters in the constructor this way as well.
class User(db.Model):
...
def __init__(self, username, email):
self.username = username
self.email = email
self.password = generate_random_password()
self.last_login = None

Categories

Resources