My flask app centers around modifying models based on SQLAlchemy. Hence, I find flask-admin a great plugin because it maps my SQLA models to forms with views already defined with a customizable interface that is tried and tested.
I understand that Flask-admin is intended to be a plugin for administrators managing their site's data. However, I don't see why I can't use FA as a framework for my users to CRUD their data as well.
To do this, I have written the following:
class AuthorizationRequiredView(BaseView):
def get_roles(self):
raise NotImplemented("Override AuthorizationRequiredView.get_roles not set.")
def is_accessible(self):
if not is_authenticated():
return False
if not current_user.has_role(*self.get_roles()):
return False
return True
def inaccessible_callback(self, name, **kwargs):
if not is_authenticated():
return current_app.user_manager.unauthenticated_view_function()
if not current_user.has_role(*self.get_roles()):
return current_app.user_manager.unauthorized_view_function()
class InstructionModelView(DefaultModelView, AuthorizationRequiredView):
def get_roles(self):
return ["admin", "member"]
def get_query(self):
"""Jails the user to only see their instructions.
"""
base = super(InstructionModelView, self).get_query()
if current_user.has_role('admin'):
return base
else:
return base.filter(Instruction.user_id == current_user.id)
#expose('/edit/', methods=('GET', 'POST'))
def edit_view(self):
if not current_user.has_role('admin'):
instruction_id = request.args.get('id', None)
if instruction_id:
m = self.get_one(instruction_id)
if m.user_id != current_user.id:
return current_app.user_manager.unauthorized_view_function()
return super(InstructionModelView, self).edit_view()
#expose('/delete/', methods=('POST',))
def delete_view(self):
return_url = get_redirect_target() or self.get_url('.index_view')
if not self.can_delete:
return redirect(return_url)
form = self.delete_form()
if self.validate_form(form):
# id is InputRequired()
id = form.id.data
model = self.get_one(id)
if model is None:
flash(gettext('Record does not exist.'), 'error')
return redirect(return_url)
# message is flashed from within delete_model if it fails
if self.delete_model(model):
if not current_user.has_role('admin') \
and model.user_id != current_user.id:
# Denial: NOT admin AND NOT user_id match
return current_app.user_manager.unauthorized_view_function()
flash(gettext('Record was successfully deleted.'), 'success')
return redirect(return_url)
else:
flash_errors(form, message='Failed to delete record. %(error)s')
return redirect(return_url)
Note: I am using Flask-User which is built on top of Flask-Login.
The code above works. However, it is difficult to abstract as a base class for other models which I would like to implement access control for CRUD operations and Index/Edit/Detail/Delete views.
Mainly, the problems are:
the API method, is_accessible, does not provide the primary key of the model. This key is needed because in almost all cases relationships between users and entities are almost always stored via relationships or in the model table directly (i.e. having user_id in your model table).
some views, such as delete_view, do not provide the instance id that can be retrieve easily. In delete_view, I had to copy the entire function just to add one extra line to check if it belongs to the right user.
Surely someone has thought about these problems.
How should I go about rewriting this to something that is more DRY and maintainable?
Related
I make a tuition payment app with Flask and using Flask-admin as database management for every school who have registered with my app.
I already make it, but now I'm really not sure how to separate data access between every school account who have registered to manage their own tuition payment.
With Flask-login, I can do it with this code:
#app.route('/school_admin')
#login_required
def school_admin():
school_data = db.session.query(Student.id, Student.name).filter_by(school_id=current_user.id).all()
return render_template('school_admin_dashboard.html', school_data=school_data)
But, because Flask-admin generates it table views automatically, I'm really not sure how to do that..?
So far, I have made my ModelView like this:
class SchoolAdminModelView(sqla.ModelView):
def is_accessible(self):
if not current_user.is_active or not current_user.is_authenticated:
return False
if current_user.has_role('schooladmin'):
return True
return False
def _handle_view(self, name, **kwargs):
if not self.is_accessible():
if current_user.is_authenticated:
# permission denied
abort(403)
else:
# login
return redirect(url_for('security.login', next=request.url))
class StudentModelView(SchoolAdminModelView):
pass
admin.add_view(StudentModelView(Student, db.session))
So, how to separate view model by school_id..?
NOTE: my reason why am I using Flask-admin instead of Flask-login for school Admin because of it pretty simple to manage user and superuser roles.
And I have using Flask-login for Parent of the student who is the user who consumes this App.
Assuming that the logged in user has a school_id connected to them:
class SchoolAdminModelView(sqla.ModelView):
def get_query(self):
return super(SchoolAdminModelView, self).get_query().filter(School.id == current_user.school_id)
It filters the query which is used to generate the list of items in the table. You might have to adjust it a bit to your exact needs.
Your code almost works #Joost, but I added a little bit method to it.
I've found the solution here and modify it a little bit, this is the code how to separate the data access between different school using this way:
class StudentModelView(sqla.ModelView):
def get_query(self):
return Student.query.filter_by(school_id=current_user.id)
def get_count_query(self):
return self.session.query(func.count('*')).select_from(self.model).filter(
Student.school_id == current_user.id
)
I have a user profile model that is storing configurations for a number of third party API keys, and I'm trying to determine how to best go about dynamically generating forms based on the choice that the user makes. The app supports only a subset of services, so I'm using a CharField (+ CHOICES) to narrow down what the user is trying to submit a configuration for. The user can submit as many duplicates as they would like (3 sets of Cloud Service 1 keys, for example)
I have this model:
class ServiceIntegration(models.Model):
profile = models.ForeignKey(
Profile,
on_delete=models.CASCADE
)
CS1 = 'CS1'
CS2 = 'CS2'
SERVICE_CHOICES = (
(CS1, 'Cloud Service 1'),
(CS2, 'Cloud Service 2'),
)
alias = models.CharField(max_length=255)
service = models.CharField(
max_length=255,
choices=SERVICE_CHOICES,
default=CS1
)
config = JSONField()
In a form, the user has a dropdown whose QuerySet is set to this model's objects. When the user makes a selection, I'd like to reach out to an endpoint and dump some form HTML in a predetermined location. Presumably, I could have a form set up for each integration, and simply have a view that takes in the choice, looks up the form, and renders it back out (same for POSTing data to that endpoint).
What's my best bet to render forms (and accept data) dynamically based on a user's choice?
I ended up writing a BaseService class that each integration implementation inherits from, and a form that each implementation returns like:
from service_integrations.forms import CloudServiceOneForm
class BaseService(ABC):
def __init__(self, configuration):
self.configuration = configuration
super().__init__()
#abstractmethod
def get_form(self):
pass
#abstractmethod
def get_service_name(self):
pass
#abstractmethod
def check(self):
pass
#abstractmethod
def run(self):
pass
class CloudServiceOne(BaseService):
def get_service_name(self):
return 'Cloud Service One' # This should match the CharField Choice
def get_form(self):
return CloudServiceOneForm()
def check(self):
# Requirements for config go here
if self.configuration.get('secret_key') and self.configuration.get('access_key'):
return True
else:
return False
def run(self):
pass
As well as a utility that can be called from a view to pass the relevant form into a context:
from service_integrations.classes import BaseService
def get_service(service):
# Passes empty configurations to each subclass of BaseService and returns the handler based on the name.
for cls in BaseService.__subclasses__():
if cls({}).get_service_name() == service:
return cls
return None
Then you can pass the form into the context like:
service = get_service('Cloud Service One')
context = {
'form': service.get_form()
}
return render(request, 'some_template.html', context)
I have a DRF ViewSet to which I am adding the CanViewAndEditStaff permission. I want only certain users (user.access_level < 2) to be able to view the list of staff. In my Permissions class, how can I differentiate between a call to the list view and to the get item view. Here is my permissions class:
class CanViewAndEditStaff(permissions.BasePermission):
def has_permission(self, request, view):
# IF THIS IS A LIST VIEW, CHECK ACCESS LEVEL
if ( request.user.access_level < 3 ):
return True
# ELSE, CONTINUE ON TO OBJECT PERMISSIONS
def has_object_permission(self,request,view,account):
# admin can do anything
if ( request.user.access_level == 1 ):
return True
# view/edit/delete
else:
# users can view their own account
if account == request.user:
return True
elif account.access_level >= request.user.access_level:
return True
return False
class CanViewAndEditStaff(permissions.BasePermission):
def has_permission(self, request, view):
# IF THIS IS A LIST VIEW, CHECK ACCESS LEVEL
if (view.action == 'list' and request.user.access_level < 3 ):
return True
# ELSE, CONTINUE ON TO OBJECT PERMISSIONS
you can use view.action to know if this is list or something else.
This doesn't exactly address the question, but this technique is applicable.
I used a variation on Ykh's answer that allows the same permission class to be used broadly across many views which display a variety of different models.
In my view class I added an attribute to distinguish the originating view, thus allowing the appropriate object comparison to determine permissions
# views.py
class SomeView(ListAPIView):
permission_classes = (IsPermd, )
is_some_view = True
class SomeOtherView(RetrieveAPIView
permission_classes = (IsPermd, )
is_some_other_view = True
# permissions.py
class IsPermd(BasePermission):
def has_object_permissions(self, request, view, obj):
if hasattr(view, 'is_some_view'):
# whatever special considerations
if hasattr(view, 'is_some_other_view'):
# whatever other special considerations
This feels a little clunky, but until I find a better way I'll stick with it.
I have the application in Django REST as backend and Angular as frontend.
Suppose in This is my code
class ModelClass (models.Model):
name = models.CharField(max_length=100)
email = models.EmailField()
def save(self, *args, **kwargs):
#check if the row with this hash already exists.
if not self.pk:
self.hash = self.create_hash()
self.my_stuff = 'something I want to save in that field'
# call to some async task
super(ModelClass, self).save(*args, **kwargs)
In my REST i have this view
class ModelListCreateView(generics.ListCreateAPIView):
model = ModelClass
serializer_class = ModelClassSerializer
def pre_save(self, obj):
obj.created_by = obj.updated_by = self.request.user.staff
def post_save(self, obj, created=False):
# add some other child objects of other model
I don't want to do unit testing. I want to do system testing so that I need to know if I post something to that view then
Pre-save thing should work
Record gets created
Save method of Model gets called with his stuff
After save method in REST gets called
Then I can assert all that stuff.
Can I test all that . I want to know which thing I need to have that sort of test rather than small unit tests
I am confused do I need to use Django test or REST Test or selenium test or PyTEst or factory boy because i want to know if things are actually getting in database
What you are looking is some kind of a REST Client code that would then be able to run your tests and you would be able to verify if the call is successful or not. Django Rest Framework has the APIRestFactory helper class that will aid you in writing such tests. The documentation can be found here and specifically look at the Example section. This would be part of your Django tests
I am using Flask and WTforms in App Engine, trying to implement uniqueness contraint on one of the field. The question is big, please be patient and I have been stuck here from many hours, need some help from you people. Started learning App Engine, Flask and WTForms a month ago. Thanks in advance.
Application has model 'Team' as shown below:
class Team(db.Model):
name = db.StringProperty(required=True)
-- some other fields here --
Requirement: Name of the team has to be unique.
I have followed the links
http://www.codigomanso.com/en/2010/09/solved-anadir-claves-unicas-en-google-app-engine-en-3-lineas/
http://squeeville.com/2009/01/30/add-a-unique-constraint-to-google-app-engine/
http://csimms.botonomy.com/2012/07/there-are-only-two-ways-to-enforce-unique-constraints-in-google-app-engine.html
Have come up with the following code:
models.py: Created a separate table 'Unique' as given in the link:
class Unique(db.Model):
""" Handles uniqueness constriant on a field """
#classmethod
def unique_check(cls, form_name, field_data):
def tx(form_name, field_data):
key_name = "%s%s" % (form_name, field_data)
uk = Unique.get_by_key_name(key_name)
app.logger.debug("UK:" + str(uk))
if uk:
return False
uk = Unique(key_name=key_name)
uk.put()
return True
ret_val = db.run_in_transaction(tx, form_name, field_data)
app.logger.debug("ret_val:" + str(ret_val))
return ret_val
forms.py: I have overridden the __init__() and validate_on_submit() function in which uniqueness is checked and if it is not unique, error is attached to that field and validation error will be raised in the same way as wtforms's validators.
class TeamForm(wtf.Form):
def __init__(self, *args, **kwargs):
super(TeamForm, self).__init__(*args, **kwargs)
if kwargs.get('edit', None):
self.old_name = self.name.data.lower()
def validate_on_submit(self, edit=False):
if not super(TeamForm, self).validate_on_submit():
return False
if edit:
if self.old_name and self.old_name != self.name.data.lower():
Unique.delete_entity(self.__class__.__name__, self.old_name)
if not Unique.unique_check(self.__class__.__name__, self.name.data.lower()):
self.name.errors.append("Value '%s' is not unique" % self.name.data)
return False
else:
if not Unique.unique_check(self.__class__.__name__, self.name.data.lower()):
self.name.errors.append("Value '%s' is not unique" % self.name.data)
return False
return True
**---- Form fields declaration ----**
The above code works when new team is inserted.I mean it checks uniqueness properly. The problem occurs, when user edits the team information. Following two scenarios are problematic:
When the user tries to submit the form, application will throw "Not unique" error, it is obvious because "Unique" table has "key_name" for this team.
If user changes "team name", application has to delete the previous team name from the "Unique" table and has to check uniqueness for the "changed team name". I am not able to handle these two scenarios.
My edit_team function looks like this:
#app.route('/team/edit/<key>', methods=['GET','POST'])
#login_required
def edit_team(key):
k = db.Key(key)
team = db.get(k)
form = TeamForm(obj = team, edit=True) # to save old name, doesn't work.
if form.validate_on_submit(edit=True): # edit=True is given only in edit function
team.name = form.name.data
-- others fields are updated in the similar way --
team.put()
return redirect(url_for('teams_list'))
return render_template('edit_team.html', form=form)
Problem can be easily solved if I am able to find out 'old name' of the team, so that I can delete it from the "Unique" table. As you can see I am saving old name of the team in TeamForm __init__() function, but __init__() is called during GET(old name is saved) and also in POST(modified name will get saved!!). So, I cannot find out old name at all and it remains in the "Unique" table, nobody can use this "old team name" anymore.
I tried to explain as much as possible, please let me know if you want more info.
Edit: didn't answer your question properly the first time.
Separate instances of the Form object will be instantiated for the GET and POST requests, so you can't save the old_name to self.
You'll need to pass the old_name to the browser in the form, and have the browser submit the old_name back in the POST request.
The easyish way to do this is to create a hidden form field that the user doesn't see, but will get submitted by the POST request. I'm not too familiar with WTForms but I assume you can initialize the old_name field value in your the GET request handler.