Checking uniqueness contraint during form validation in App Engine - python

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.

Related

Django-Select2 Heavy Widget

I was trying to implement Django-select2 for the first time.... I referred their documentation and some of the stack overflow solutions to implement it.... I managed to get ajax functionality work properly, also i am able to select multiple choices... however when I submit and validate the form, I am getting error like -> "Select a valid choice. 123456 is not one of the available choices."
I am not understanding what I am doing wrong....
here is my form.
class MyCustReqForm(forms.ModelForm):
initial_customer = forms.MultipleChoiceField(
widget=HeavySelect2MultipleWidget(data_view='customer_ajax',
attrs={'data-minimum-input-length': 4, 'delay':200},
model=Customer),
)
end_customer = forms.MultipleChoiceField(
widget=HeavySelect2MultipleWidget(data_view='customer_ajax',
attrs={'data-minimum-input-length': 4, 'delay':200},
model=Customer),
)
class Meta:
model = Workflow_Customer
fields = [ 'initial_customer', 'end_customer' ]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['initial_customer'].widget.attrs.update({'style': 'width:100%', 'data-placeholder': 'Select Customer'})
self.fields['end_customer'].widget.attrs.update({'style':'width:100%', 'data-placeholder':'Select end customer'})
and customer_ajax view calls below function...
def customer_select2(request):
term = request.GET.get("term", None)
if term:
res = list(Customer.objects.filter(Q(customer_number__contains=term) | Q(customer_name__contains=term)).values('id', 'customer_number', 'customer_name'))[:10]
if res:
result = [{'id': value['id'], 'text': value['customer_number'] + ' ' + value['customer_name'] } for index, value in enumerate(res)]
return JsonResponse({'err': 'nil', 'results': result}, safe=False)
return JsonResponse(data={'success': False, 'errors': 'No mathing items found'})
when I checked in debug mode. I found that choices are empty...
I appreciate for the quick help... if possible, please provide one complete example which explains how form defined and view used for Ajax function...
That happens because when you send your form there aren't initial choices so the chosen option don't pass the validation. When you send your form, the class MyCustReqForm is initialized and in that moment you must do a requets to get choices of your form field. That choices must containt your chosen option. For example:
self.fields['customer'].choices = call_to_end_point()
When the request is 'GET' there isn't problem, but in the request 'POST' is necessary to have some basic options.
PD: Sorry for my english
Solution is to write custom clean methods so this way you are defining your own validations for a field thus overriding django form validation for the choice field.
def clean_initial_customer(self):
choices = self.cleaned_data["initial_customer"]
# write your custom logics and remove any errors
let me know if you find some issue in implementing the same.

Add custom fields in Django admin for every model

I'm working on my personal website and wanted to handle both frontend and database translations (ie having everything in 2-3 languages). While there are already quite a few third-party apps for handling translation in the database, I thought it'd be both fun and interesting to build my own.
I'm at the last step. So far, everything's been working fine. Here's a quick recap on how it works (skipping over a few things):
I have a "translation" app in my django project
It provides a few different tables, most notably :
Language: list of available languages
Item: equivalent to saying "This is the 'name' field for the instance 33 of the 'Job' model"
Translation: a ManyToMany between Languages and Items. Basically all the translations for a given item.
This app provides an abstract model called "Translation model"
Every model from your regular apps that have fields to be translated must inherit from this model
Through signals and FK relationship, the app then handles both the creation and deletion of entries in the Item and Translation tables (automatically)
Ok so let's say we have a "Job" model that has 2 fields that must be translated: title and description. Now whenever I create a new entry in Job, here is what happens automatically:
2 new entries are created in Item:
"Job n°X, name"
"Job n°X, description"
4 entries are create in Translation :
"Job n°X, name, French"
"Job n°X, description, French"
"Job n°X, name, English"
"Job n°X, description English"
My issue is as follows:
I have many tables like "Job", that will trigger new entries in Item and then Translation
These tables all have ForeignKey towards the Item table, but Item does not have ForeignKey to these tables (since the same column stores data from different tables)
What I want to do is:
On the admin, when I go on a table like "Job", I want to see and directly update its translation texts (Translation.text). Which means seeing its 4 different "Translation" entries mentioned above. I already have a function that get the Translation instances, now it's more a problem of showing and editing them in the admin.
Rather than overriding manually each model/admin form, is there a way to apply this change globally.
My idea would be to "override the general change_form.html by adding a new zone dedicated to translation, which gets all the Translation entry related to an instance" (only if this instance is subject to translation). But not sure how to do that.
(Note that I already automatically detect all the models that require translations, and can easily get their specific fields)
Any help would be appreciated :)
For anyone that stumbles upon this topic, I've managed to solve this issue and publish my own application for database translation. The code is fully open-source and available here: https://pypi.org/project/django-database-translation/
As for the problem mentioned in my original post, I managed to apply the following logic:
I created a ModelForm that dynamically generates the "Fields to translate" for an item
I created a ModelAdmin that uses this forms and generates the same fieldnames as the above form
As a result, if I go on my "Job" item in the admin, I have access to all of its translation and can directly change them from this page
Here are the snippets, also available on https://github.com/Jordan-Kowal/django_database_translation
class DynamicTranslationForm(forms.ModelForm):
"""
Form to use in a ModelAdmin for any model that has fields to translate.
It will allow you to display and edit the Translation instances linked to the object.
Since fields are dynamically generated, you must override the get_fieldsets method in the admin (or else they won't show)
The "TranslatedAdmin" ModelAdmin natively use this form.
"""
# ----------------------------------------
# Core Methods
# ----------------------------------------
def __init__(self, *args, **kwargs):
"""Overridden method to dynamically add a new field for each Translation linked with our object"""
super(DynamicTranslationForm, self).__init__(*args, **kwargs)
if self.instance.pk:
self.set_translation_info()
for translation in self.translations:
self.fields[translation["fieldname"]] = translation["field"]
self.initial[translation["fieldname"]] = translation["instance"].text
def save(self, commit=True):
"""Overridden method to save the updated Translation texts"""
if self.instance.pk:
for translation in self.translations:
obj = translation["instance"]
fieldname = translation["fieldname"]
value = self.cleaned_data[fieldname]
obj.text = value
obj.save()
return super(DynamicTranslationForm, self).save(commit=commit)
# ----------------------------------------
# Custom Methods
# ----------------------------------------
def set_translation_info(self):
"""
Finds all the Translation instances linked to our object, and stores their info in an attribute
The attribute is a list of dict, each dict containing the information of one translation
"""
obj = self.instance
information = []
translations = obj.get_translations()
for translation in translations:
fieldname = create_translation_fieldname(translation)
information.append({
"instance": translation,
"fieldname": fieldname,
"field": forms.CharField(required=False, widget=forms.Textarea)
})
self.translations = information
# ADMIN.PY
class TranslatedAdmin(admin.ModelAdmin):
"""
ModelAdmin to use as parent for any model that has fields to translate
It comes with the "DynamicTranslationForm" and custom methods to display its fields
"""
# ----------------------------------------
# Config
# ----------------------------------------
form = DynamicTranslationForm
# ----------------------------------------
# Detail View
# ----------------------------------------
fieldsets = []
# ----------------------------------------
# Custom Methods
# ----------------------------------------
def get_form(self, request, obj=None, **kwargs):
"""Required for get_fieldsets"""
kwargs['fields'] = flatten_fieldsets(self.fieldsets)
return super().get_form(request, obj, **kwargs)
def get_fieldsets(self, request, obj=None):
"""
Allows us to display the field dynamically created by "DynamicTranslationForm"
The fieldnames in "DynamicTranslationForm" and this function must be identical
In other words:
- "DynamicTranslationForm" creates the fields
- This function adds fields with the same name to the fieldsets
- As a result, the fields now appear on the form
"""
fieldsets = self.fieldsets.copy()
# Get current Item
url = request.build_absolute_uri("?")
if url.endswith("/change/"):
url = url[:-8]
object_id = url.split("/")[-1]
obj = self.model.objects.get(pk=object_id)
# Create a field for each translation associated with our object
fields = []
translations = obj.get_translations()
for translation in translations:
fieldname = create_translation_fieldname(translation)
fields.append(fieldname)
# Add a fieldset with our fields
fieldsets.append(['TRANSLATIONS', {'fields': fields}])
return fieldsets

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

How to populate choice form from db in Django?

I can't figure out how to populate choice form from db. I know about ModelChoiceForm but the problem seems to be slightly different.
I want user to choose which sector does he work in. For example: 'Finance','Electronics' etc. which I would do simple:
SECTOR_CHOICES = (('finance',_('Finance'),
'electronics',_('Electronics')...
))
But the problem is that I want admin of the web to be able to add new choices, remove choice etc.
What came to my mind is to create a simple Model called Sector:
class Sector(models.Model):
name = models.CharField(max_length=40)
and User would have new attribute sector = models.ModelChoice(Sector).
But I'm scared what would happend when admin changes or removes a sector which is already used, and more, what if he removes it and the sector attribute is required?
How to solve this problem?
I would just override the delete_model as custom action and there check if the selected sector object is in use.
def delete_model(modeladmin, request, queryset):
for obj in queryset:
if UserModel.objects.filter(sector=obj).exists():
# do not delete, just add some message warning the admin about it
else:
obj.delete()
class UserModelAdmin(admin.ModelAdmin):
actions = [delete_model]
# ...

How to validate django form only when adding not editing

How could we make the django form to not validate if we are editing, not adding a new record. The code as following :
class PageForm(forms.Form):
name = forms.CharField(max_length=100,widget=forms.TextInput(attrs={'class':'textInput'}))
description = forms.CharField(max_length=300, required=False,widget=forms.TextInput(attrs={'class':'textInput'}))
body = forms.CharField(widget=forms.Textarea)
template = forms.CharField(max_length=30,widget=forms.TextInput(attrs={'class':'textInput'}))
navbar = forms.BooleanField(required=False, widget=forms.Select(choices=(('True','True'),
('False', 'False'))))
publish = forms.BooleanField(widget=forms.Select(choices=(('Published','Publish Now'),
('Private','Private'),
('Draft','Draft'))))
def save(self, page=None, commit=True):
data = self.cleaned_data
if not page:
page = models.Page(key_name=data['name'].replace(' ','-'))
page.name = data['name']
page.description = data['description']
page.body = data['body']
page.template = data['template']
page.publish = data['publish']
if commit: page.put()
return page
# prevent the same page 's name
def clean_name(self):
name = self.cleaned_data['name']
query = models.Page.all(keys_only=True)
query.filter('name = ', name)
page = query.get()
if page:
raise forms.ValidationError('Page name "%s" was already used before' % name)
return name
The purpose of this name validation is to prevent the records with the same name. BUt i found that, it also validate on edit, so we couldn't edit records, since it will said 'records with same name already exist'.
Actually for editing, the page param on save function wont be none, but prev record instead, and wil be none on saving a new one. But how we read this param, on clean_name function so we can now whether it is editing or creating?
Thanks a lot!
in your clean method, you can use self.initial to know whether it is adding or editing. If it is editing, the self.initial will not be empty. But when it is adding, self.initial will be dictionary of what the previous value.
If you are editing form, then the form has some instance, and you can check if that exists.
If it does, then you are probably editing existing object.. right?
Example:
If you are editing object with form, you create form object much like this:
form = MyForm(instance = myobject)
Then in your form class methods you can check if form has saved instance in a way that it is described here:
Test if Django ModelForm has instance
in your clean_name function exclude the current object from queryset
query.filter('name = ', name).exclude(pk=self.pk)
or change the if condition to check that page and current object are not the same.
Sorry, I couldn't comment below your guys post, don't know why.
#sunn0 : I didn't use django models, coz deploy the app in appengine, so use appengine model instead.
#Zayatzz : May you show a little code how to do it? Since whether we are adding or editing, we always bound the form to request.POST before validation, so don't know how to differentiate.
#Ashok : I made a workaround based on your suggestion. Since previously I didn't pass the pk to form, but passing the prev object as param instead, so couldn't exclude by using pk. So, I change the code and put additional key as pk (if create, let key empty, but if edit fill key with pk) and just check in if condition, if key field not empty, then it means we are editing. Not sure if it is best practice, but it works anyway.
I can suggest to override form's init method
https://stackoverflow.com/a/70845558/15080117
because there is an argument instance.

Categories

Resources