Flask Admin - Access old values using on_model_change() - python

My goal is to perform some additional action when a user changes a value of a existing record.
I found on_model_change() in the docs and wrote the following code:
def on_model_change(self, form, model, is_created):
# get old and new value
old_name = model.name
new_name = form.name
if new_name != old_name:
# if the value is changed perform some other action
rename_files(new_name)
My expectation was that the model parameter would represent the record before the new values from the form was applied. It did not. Instead i found that model always had the same values as form, meaning that the if statement never was fulfilled.
Later i tried this:
class MyView(ModelView):
# excluding the name field from the form
form_excluded_columns = ('name')
form_extra_fields = {
# and adding a set_name field instead
'set_name':StringField('Name')
}
...
def on_model_change(self, form, model, is_created):
# getting the new value from set_name instead of name
new_name = form.set_name
...
Although this solved my goal, it also caused a problem:
The set_name field would not be prefilled with the existing name, forcing the user to type the name even when not intending to change it
I also tried doing db.rollback() at the start of on_model_change() which would undo all changes done by flask-admin, and make model represent the old data. This was rather hacky and lead my to reimplement alot of flask admin code myself, which got messy.
What is the best way to solve this problem?
HOW I SOLVED IT
I used on_form_prefill to prefill the new name field instead of #pjcunningham 's answer.
# fill values from model
def on_form_prefill(self, form, id):
# get track model
track = Tracks.query.filter_by(id=id).first()
# fill new values
form.set_name.data = track.name

Override method update_model in your view. Here is the default behaviour if you are using SqlAlchemy views, I have added some notes to explain the model's state.
def update_model(self, form, model):
"""
Update model from form.
:param form:
Form instance
:param model:
Model instance
"""
try:
# at this point model variable has the unmodified values
form.populate_obj(model)
# at this point model variable has the form values
# your on_model_change is called
self._on_model_change(form, model, False)
# model is now being committed
self.session.commit()
except Exception as ex:
if not self.handle_view_exception(ex):
flash(gettext('Failed to update record. %(error)s', error=str(ex)), 'error')
log.exception('Failed to update record.')
self.session.rollback()
return False
else:
# model is now committed to the database
self.after_model_change(form, model, False)
return True
You'll want something like the following, it's up to you where place the check, I've put it after the model has been committed:
def update_model(self, form, model):
"""
Update model from form.
:param form:
Form instance
:param model:
Model instance
"""
try:
old_name = model.name
new_name = form.name.data
# continue processing the form
form.populate_obj(model)
self._on_model_change(form, model, False)
self.session.commit()
except Exception as ex:
if not self.handle_view_exception(ex):
flash(gettext('Failed to update record. %(error)s', error=str(ex)), 'error')
log.exception('Failed to update record.')
self.session.rollback()
return False
else:
# the model got committed now run our check:
if new_name != old_name:
# if the value is changed perform some other action
rename_files(new_name)
self.after_model_change(form, model, False)
return True
There are similar methods you can override for create_model and delete_model.

Related

How to override delete logic on Flask Admin?

I have been searching on Google and StackOverflow about this. Basically, I want to try and override the delete function on Flask-Admin to not actually delete a record, but instead update a row of the object called 'deleted_by' and 'deleted_on'.
I have found a few questions on StackOverflow that explain how to change the logic on the save button by using on_model_change, but no one specific about the delete model logic. I have also not found any information regarding this on the documentation. Could anyone show me how should I handle this issue?
Thanks in advance!
Override method delete_model in your view. Here is the default behaviour if you are using Sqlalchemy views, note the call to self.session.delete(model) in the try ... except block.
def delete_model(self, model):
"""
Delete model.
:param model:
Model to delete
"""
try:
self.on_model_delete(model)
self.session.flush()
self.session.delete(model)
self.session.commit()
except Exception as ex:
if not self.handle_view_exception(ex):
flash(gettext('Failed to delete record. %(error)s', error=str(ex)), 'error')
log.exception('Failed to delete record.')
self.session.rollback()
return False
else:
self.after_model_delete(model)
return True
You would need something like the following in your view:
class MyModelView(ModelView):
def delete_model(self, model):
"""
Delete model.
:param model:
Model to delete
"""
try:
self.on_model_delete(model)
# Add your custom logic here and don't forget to commit any changes e.g.
# self.session.commit()
except Exception as ex:
if not self.handle_view_exception(ex):
flash(gettext('Failed to delete record. %(error)s', error=str(ex)), 'error')
log.exception('Failed to delete record.')
self.session.rollback()
return False
else:
self.after_model_delete(model)
return True
Also, you might not want to bother with the self.on_model_delete(model) and self.after_model_delete(model) calls because by default they do nothing.

Check if record exists in Django Rest Framework API LIST/DATABASE

I want to create a viewset/apiview with a path like this: list/<slug:entry>/ that once I provide the entry it will check if that entry exists in the database.
*Note: on list/ I have a path to a ViewSet. I wonder if I could change the id with the specific field that I want to check, so I could see if the entry exists or not, but I want to keep the id as it is, so
I tried:
class CheckCouponAPIView(APIView):
def get(self, request, format=None):
try:
Coupon.objects.get(coupon=self.kwargs.get('coupon'))
except Coupon.DoesNotExist:
return Response(data={'message': False})
else:
return Response(data={'message': True})
But I got an error: get() got an unexpected keyword argument 'coupon'.
Here's the path: path('check/<slug:coupon>/', CheckCouponAPIView.as_view()),
Is there any good practice that I could apply in my situation?
What about trying something like this,
class CheckCouponAPIView(viewsets.ModelViewSet):
# other fields
lookup_field = 'slug'
From the official DRF Doc,
lookup_field - The model field that should be used to for performing
object lookup of individual model instances. Defaults to pk

Flask Admin editing columns send requests

I am using flask-admin with ModelViews
class MyModel(ModelView):
can_create = False
can_edit = True
column_list = ['column']
This allows me to edit the data on each row. However I want to perform some custom function in addition to the editing. I tried to add a route for the edit but it overrides the existing functionality.
#app.route('/admin/mymodelview/edit/', methods=['POST'])
def do_something_in_addition():
...
Is there any way to extend the existing edit functionality?
Override either the after_model_change method or the on_model_change methods in your view class.
For example :
class MyModel(ModelView):
can_create = False
can_edit = True
column_list = ['column']
def after_model_change(self, form, model, is_created):
# model has already been commited here
# do custom work
pass
def on_model_change(self, form, model, is_created)
# model has not been commited yet so can be changed
# do custom work that can affect the model
pass

save() prohibited to prevent data loss due to unsaved related object

I need to pass a primary key from a newly created ModelForm to another form field in the same view but I get an error. Any suggestions to make this work?
It looks like in the past, this would be the answer:
def contact_create(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse(contact_details, args=(form.pk,)))
else:
form = ContactForm()
From the documentation, this is what is happening in the newer Django version > 1.8.3
p3 = Place(name='Demon Dogs', address='944 W. Fullerton')
Restaurant.objects.create(place=p3, serves_hot_dogs=True, serves_pizza=False)
Traceback (most recent call last):
...
ValueError: save() prohibited to prevent data loss due to unsaved related object 'place'.
This is how I am getting my pk from the view:
my_id = ""
if form.is_valid():
# deal with form first to get id
model_instance = form.save(commit=False)
model_instance.pub_date= timezone.now()
model_instance.user= current_user.id
model_instance.save()
my_id = model_instance.pk
if hourformset.is_valid():
hourformset.save(commit=False)
for product in hourformset:
if product.is_valid():
product.save(commit=False)
product.company = my_id
product.save()
else:
print(" modelform not saved")
return HttpResponseRedirect('/bizprofile/success')
it is simple:
p3 = Place(name='Demon Dogs', address='944 W. Fullerton')
p3.save() # <--- you need to save the instance first, and then assign
Restaurant.objects.create(
place=p3, serves_hot_dogs=True, serves_pizza=False
)
This was introduced in Django 1.8. Previously you could assign not saved instance to One-To-One relation and in case of fail it was silently skipped. Starting from Django 1.8 you will get error message in this case.
Check a documentation of Django 1.7 -> 1.8 upgrade.
It says:
Assigning unsaved objects to a ForeignKey, GenericForeignKey, and
OneToOneField now raises a ValueError.
If you are interested in more details, you can check save method in django.db.models.base: Some part of it:
for field in self._meta.concrete_fields:
if field.is_relation:
# If the related field isn't cached, then an instance hasn't
# been assigned and there's no need to worry about this check.
try:
getattr(self, field.get_cache_name())
except AttributeError:
continue
obj = getattr(self, field.name, None)
# A pk may have been assigned manually to a model instance not
# saved to the database (or auto-generated in a case like
# UUIDField), but we allow the save to proceed and rely on the
# database to raise an IntegrityError if applicable. If
# constraints aren't supported by the database, there's the
# unavoidable risk of data corruption.
if obj and obj.pk is None:
raise ValueError(
"save() prohibited to prevent data loss due to "
"unsaved related object '%s'." % field.name
)
Last 5 rows are where this error is raised. basically your related obj which is not saved will have obj.pk == None and ValueError will be raised.
Answered - The problem arose from django not saving empty or unchanged forms. This led to null fields on those unsaved forms. Problem was fixed by allowing null fields on foreign keys, as a matter of fact -- all fields. That way, empty or unchanged forms did not return any errors on save.
FYI: Refer to #wolendranh answer.
I just removed my model.save() and the error went away.
This only works if you are saving it when there are no changes.
Otherwise you should save it one time, if you changed something in the queryset.
an example:
views.py
queryset = myModel.objects.get(name="someName", value=4)
queryset.value = 5
# here you need to save it, if you want to keep the changes.
queryset.save()
...
# If you save it again, without any changes, for me I got the error save() prohibited to prevent data loss due to unsaved related object
# don't save it again, unless you have changes.
# queryset.save()

Checking uniqueness contraint during form validation in App Engine

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.

Categories

Resources