Django form update optimistic locking, version based - django

I've got django model and view implemented like here: (+mysql db)
class MyModel(models.Model):
name = models.CharField(max_length=100)
version = models.IntegerField(default=1, editable=False)
def updateModel(request, id):
toUpdate = MyModel.objects.get(pk=id)
if request.method=='POST':
form = MyModelForm(request.POST, instance=toUpdate)
if form.is_valid():
actual = MyModel.objects.get(pk=id)
if (actual.version == form.instance.version):
form.instance.version = form.instance.version+1
form.save()
return redirect('somewhere')
else:
#some error
form = MyModelForm(instance=toUpdate)
return render(request, 'somwhere2/createupdate.html', {'form':form})
The scenario is: - current model values: name="aaa", version=1,
2 users open edit form, first user changes name "aaa" to "bbb", and saves, second changes name "aaa" co "ccc" and saves. Result is "ccc", but I'd like to have some message/version conflict message... The problem is.. there is no conflict, because even if the second user can see still "aaa", while in DB there is "bbb" already... but after POST button click, the values are updated to bbb first, and version is updated, so the code is unable to see, that user2 works on old version... :(
I'd like that versioning mechanism to prevent such scenario, but I'm unable to achieve it...
How to implement it?
I've read everything I could about django optimistic locking etc, but unable to achieve it,

I think using select_for_update() might solve your problem.
Check out this article.
I was thinking of something like this:
def updateModel(request, id):
toUpdate = MyModel.objects.get(pk=id)
if request.method=='POST':
form = MyModelForm(request.POST, instance=toUpdate)
if form.is_valid():
with transaction.atomic():
try:
actual = MyModel.objects.filter(pk=id).select_for_update(nowait=True).get()
except OperationalError:
# raise some error
if (actual.version == form.instance.version):
form.instance.version = form.instance.version+1
form.save()
return redirect('somewhere')
else:
#some error
form = MyModelForm(instance=toUpdate)
return render(request, 'somwhere2/createupdate.html', {'form':form})

I believe I've found a problem. It's here:
in Model: version =(...) editable=False
So when field is not editable - it is not placed in form, so you're loosing information about version number... And are unable to compare initially loaded version, with actual version.
it is still not thread-safe, but in general- blocks typical attempts to edit and save form by 2 users.

Related

How to update data in Django when getting unique field error

first thing first I'm sorry for my bad english. I hope so u can understand me easily.
I'm trying to make a blog with django but there's a place I can't solve. I used unique slug field instead of id for url, whenever I want to update the data I get the UNIQUE constraint failed: post_post.url_text error (url_text is slugfield variable name). Here is the my model,
and the Form looks like this,
At first I followed a way to update the data here:
#login_required(login_url='login')
def post_update(request, url_text=None):
post = get_object_or_404(Post, url_text=url_text)
form = PostWrite(request.POST or None, instance=post)
if form.is_valid():
post_author = request.user
post_title = form.cleaned_data.get('post_title')
post_content = form.cleaned_data.get('post_content')
post_tags = form.cleaned_data.get('tags')
post = Post(post_author=post_author, post_title=post_title, post_content=post_content, tags=post_tags)
post.save()
context = {
'post': post,
'form': form,
}
return render(request, 'post/re_write.html', context)
and I got the error I mentioned at the beginning of the post. Then I found a solution like this in the forums,
if form.is_valid():
form.save()
This time it does not give any error but does not update the data. Despite hours of research, for some reason I could not find a tangible solution. I wanted to ask you esteemed coders as a last resort and I look forward to your help.
The issue is that you're creating a new post with the following code while this view appears to be an update:
post = Post(post_author=post_author, post_title=post_title, post_content=post_content, tags=post_tags)
post.save()
Instead, you should utilize the modelform you're already using to save the changes to the instance:
if form.is_valid():
post = form.save()
I'm guessing maybe because you weren't capturing the post from form.save() the rendered template appeared to not have the data updated because the instance passed into the template was from before the changes.
Other issue:
You're overriding the save method, but not always calling super().save. This means that you're only saving the post when the url_text property is not set. Instead always call super().save
def save(self, *args, **kwargs):
if ...
# other stuff
super().save(*args, **kwargs)

Django initial value for MultiChoice Field ignored for ModelForm

this is my first post here and I am very new to Django but I just can't seem to find a solution for this problem... I've searched stackoverflow and google but nothing seems to work for me...
I have a wine-app and want to be able to add and remove wines from the user's stock. In the list of wines the user can choose a wine to add and the ID of this wine is passed in the POST data. Since the data is getting lost after the first time the view is rendered I saved the ID in a cookie, which is working, but the problem is when I work with ModelForm de user has to select the foreign key for the user and for the wine, which is bad, so I tried to make it hidden and set the Fk_user and Fk_wine after the user choose the number of bottles to be added but before validation. Here's the problem after google everyone suggested I should use the "initial" and pass that to the form, but this is clearly not working because if I make the fields visible in the form I can see that it is not preselected...
viewy.py:
def addStockView(request):
wineId = request.POST.get('addStock')
if 'addStock' in request.POST:
wine = get_object_or_404(Wine, idwine=int(wineId))
userId = request.user.id
user = get_object_or_404(AuthUser, id=userId)
if request.method == 'POST':
#wineIdNew = request.COOKIES.get('wineIdToAdd')
#wineNew = get_object_or_404(Wine, idwine=wineIdNew)
form = StockForm(request.POST, initial={'fk_wine': wineNew.idwine, 'fk_auth_user': user.id})
if form.is_valid():
form.save()
return redirect('home')
else:
form = StockForm(initial={'fk_wine': wine.id,
'fk_auth_user': user.id})
response = render(request, 'addToStock.html', {'form': form})
response.set_cookie('wineIdToAdd', wineId)
return response
forms.py:
class StockForm(forms.ModelForm):
#fk_wine = ModelChoiceField(queryset=Wine.objects.all(),
# widget=HiddenInput())
#fk_auth_user = ModelChoiceField(queryset=AuthUser.objects.all(),
# widget=HiddenInput())
class Meta:
model = UserWineStock
fields = ['fk_auth_user', 'fk_wine', 'number']
can anyone help me with this..?
Yes, initial data is ignored when a form is bound to submitted data.
Instead of using initial here, you should exclude those two fields from the form and set them on the created object:
form = StockForm(request.POST)
if form.is_valid():
item = form.save(commit=False)
item.fk_wine = wine
item.fk_auth_user = request.user
item.save()
return redirect('home')
(Also, please don't call your fields things like fk_auth_user. Just call it user.)

Django form: ask for confirmation before committing to db

Update: The solution can be found as a separate answer
I am making a Django form to allow users to add tvshows to my db. To do this I have a Tvshow model, a TvshowModelForm and I use the generic class-based views CreateTvshowView/UpdateTvshowView to generate the form.
Now comes my problem: lets say a user wants to add a show to the db, e.g. Game of Thrones. If a show by this title already exists, I want to prompt the user for confirmation that this is indeed a different show than the one in the db, and if no similar show exists I want to commit it to the db. How do I best handle this confirmation?
Some of my experiments are shown in the code below, but maybe I am going about this the wrong way. The base of my solution is to include a hidden field force, which should be set to 1 if the user gets prompted if he is sure he wants to commit this data, so that I can read out whether this thing is 1 to decide whether the user clicked submit again, thereby telling me that he wants to store it.
I would love to hear what you guy's think on how to solve this.
views.py
class TvshowModelForm(forms.ModelForm):
force = forms.CharField(required=False, initial=0)
def __init__(self, *args, **kwargs):
super(TvshowModelForm, self).__init__(*args, **kwargs)
class Meta:
model = Tvshow
exclude = ('user')
class UpdateTvshowView(UpdateView):
form_class = TvshowModelForm
model = Tvshow
template_name = "tvshow_form.html"
#Only the user who added it should be allowed to edit
def form_valid(self, form):
self.object = form.save(commit=False)
#Check for duplicates and similar results, raise an error/warning if one is found
dup_list = get_object_duplicates(Tvshow, title = self.object.title)
if dup_list:
messages.add_message(self.request, messages.WARNING,
'A tv show with this name already exists. Are you sure this is not the same one? Click submit again once you\'re sure this is new content'
)
# Experiment 1, I don't know why this doesn't work
# form.fields['force'] = forms.CharField(required=False, initial=1)
# Experiment 2, does not work: cleaned_data is not used to generate the new form
# if form.is_valid():
# form.cleaned_data['force'] = 1
# Experiment 3, does not work: querydict is immutable
# form.data['force'] = u'1'
if self.object.user != self.request.user:
messages.add_message(self.request, messages.ERROR, 'Only the user who added this content is allowed to edit it.')
if not messages.get_messages(self.request):
return super(UpdateTvshowView, self).form_valid(form)
else:
return super(UpdateTvshowView, self).form_invalid(form)
Solution
Having solved this with the help of the ideas posted here as answers, in particular those by Alexander Larikov and Chris Lawlor, I would like to post my final solution so others might benefit from it.
It turns out that it is possible to do this with CBV, and I rather like it. (Because I am a fan of keeping everything OOP) I have also made the forms as generic as possible.
First, I have made the following forms:
class BaseConfirmModelForm(BaseModelForm):
force = forms.BooleanField(required=False, initial=0)
def clean_force(self):
data = self.cleaned_data['force']
if data:
return data
else:
raise forms.ValidationError('Please confirm that this {} is unique.'.format(ContentType.objects.get_for_model(self.Meta.model)))
class TvshowModelForm(BaseModelForm):
class Meta(BaseModelForm.Meta):
model = Tvshow
exclude = ('user')
"""
To ask for user confirmation in case of duplicate title
"""
class ConfirmTvshowModelForm(TvshowModelForm, BaseConfirmModelForm):
pass
And now making suitable views. The key here was the discovery of get_form_class as opposed to using the form_class variable.
class EditTvshowView(FormView):
def dispatch(self, request, *args, **kwargs):
try:
dup_list = get_object_duplicates(self.model, title = request.POST['title'])
if dup_list:
self.duplicate = True
messages.add_message(request, messages.ERROR, 'Please confirm that this show is unique.')
else:
self.duplicate = False
except KeyError:
self.duplicate = False
return super(EditTvshowView, self).dispatch(request, *args, **kwargs)
def get_form_class(self):
return ConfirmTvshowModelForm if self.duplicate else TvshowModelForm
"""
Classes to create and update tvshow objects.
"""
class CreateTvshowView(CreateView, EditTvshowView):
pass
class UpdateTvshowView(EditTvshowView, UpdateObjectView):
model = Tvshow
I hope this will benefit others with similar problems.
I will post it as an answer. In your form's clean method you can validate user's data in the way you want. It might look like that:
def clean(self):
# check if 'force' checkbox is not set on the form
if not self.cleaned_data.get('force'):
dup_list = get_object_duplicates(Tvshow, title = self.object.title)
if dup_list:
raise forms.ValidationError("A tv show with this name already exists. "
"Are you sure this is not the same one? "
"Click submit again once you're sure this "
"is new content")
You could stick the POST data in the user's session, redirect to a confirmation page which contains a simple Confirm / Deny form, which POSTs to another view which processes the confirmation. If the update is confirmed, pull the POST data out of the session and process as normal. If update is cancelled, remove the data from the session and move on.
I have to do something similar and i could do it using Jquery Dialog (to show if form data would "duplicate" things) and Ajax (to post to a view that make the required verification and return if there was a problem or not). If data was possibly duplicated, a dialog was shown where the duplicated entries appeared and it has 2 buttons: Confirm or Cancel. If someone hits in "confirm" you can continue with the original submit (for example, using jquery to submit the form). If not, you just close the dialog and let everything as it was.
I hope it helps and that you understand my description.... If you need help doing this, tell me so i can copy you an example.
An alternative, and cleaner than using a vaidationerror, is to use Django's built in form Wizard functionality: https://django-formtools.readthedocs.io/en/latest/wizard.html
This lets you link multiple forms together and act on them once they are all validated.

Django forms and MultipleChoiceField

Trying to create a form where you can sign up as a user, and add yourself to one or more categories. Getting an error while doing it:
TypeError at /users/add-user/
'categories' is an invalid keyword argument for this function
Here's my forms.py:
class AddUser(forms.Form):
name = forms.CharField()
title = forms.CharField()
website = forms.CharField(required=False)
email = forms.EmailField()
phone = forms.CharField(required=False)
company = forms.ModelChoiceField(queryset=Company.objects.all())
categories = forms.ModelMultipleChoiceField(queryset=Category.objects.all())
The last line is the one I'm having trouble with.
Here's my views.py:
def add_user(request):
if request.method == 'POST':
form = AddUser(request.POST)
if form.is_valid():
cd = form.cleaned_data
try:
p = User.objects.get(email=cd['email'])
error = "There's already a user with that e-mail adress registered. Maybe he/she is already here?"
return render_to_response('users/add_user.html', {'form': form, 'error': error}, context_instance=RequestContext(request))
except User.DoesNotExist:
p = User(name=cd['name'], title=cd['title'], website=cd['website'], email=cd['email'], phone=cd['phone'], company=cd['company'], categories=cd['categories'])
p.save()
return HttpResponseRedirect('../thanks/')
else:
form = AddUser(request.POST)
error = "You can't really submit empty forms. Try adding something useful :)"
return render_to_response('users/add_user.html', {'form': form}, context_instance=RequestContext(request))
If anyone has any suggestions to the problem (or even suggestions in general to improve my code), I'd be glad! I'm a beginner to Django and all help is appreciated.
Your problem lies here:
p = User(name=cd['name'], title=cd['title'], website=cd['website'], email=cd['email'], phone=cd['phone'], company=cd['company'], categories=cd['categories'])
The problem is that the User model doesn't include a categories field, just as it doesn't include a website or company. See the list of the available fields.
There's different approaches to handling additional data in combination with Django's auth system. Sublcassing the User class or adding a model with additional info and a one-to-one field to the User come to mind. The latter option seems to be suggested, so I'd suggest going down that path.
A bit of a nit pick, but you did ask for other suggestions. This bit:
else:
form = AddUser(request.POST)
should (IMO) be changed to this:
else:
form = AddUser()
There's no need to use request.POST for a GET request. I've never tried that but I'm guessing it will work, you just get an empty set. So not an error but possibly a source of confusion.
Your error message is also not being used. A GET on the page is not an error at all in this case, it is just how the page is initially displayed.

Django Forms with get_or_create

I am using Django ModelForms to create a form. I have my form set up and it is working ok.
form = MyForm(data=request.POST)
if form.is_valid():
form.save()
What I now want though is for the form to check first to see if an identical record exists. If it does I want it to get the id of that object and if not I want it to insert it into the database and then give me the id of that object. Is this possible using something like:
form.get_or_create(data=request.POST)
I know I could do
form = MyForm(instance=object)
when creating the form but this would not work as I still want to have the case where there is no instance of an object
edit:
Say my model is
class Book(models.Model):
name = models.CharField(max_length=50)
author = models.CharField(max_length=50)
price = models.CharField(max_length=50)
I want a form which someone can fill in to store books. However if there is already a book in the db which has the same name, author and price I obviously don't want this record adding again so just want to find out its id and not add it.
I know there is a function in Django; get_or_create which does this but is there something similar for forms? or would I have to do something like
if form.is_valid():
f = form.save(commit=false)
id = get_or_create(name=f.name, author=f.author, price=f.price)
Thanks
I like this approach:
if request.method == 'POST':
form = MyForm(request.POST)
if form.is_valid():
book, created = Book.objects.get_or_create(**form.cleaned_data)
That way you get to take advantage of all the functionality of model forms (except .save()) and the get_or_create shortcut.
You just need two cases in the view before the postback has occurred, something like
if id:
form = MyForm(instance=obj)
else
form = MyForm()
then you can call form.save() in the postback and Django will take care of the rest.
What do you mean by "if an identical record exists"? If this is a simple ID check, then your view code would look something like this:
if request.method == 'POST':
form = MyForm(request.POST)
if form.is_valid():
form.save()
else:
if get_id:
obj = MyModel.objects.get(id=get_id)
form = MyForm(instance=obj)
else:
form = MyForm()
The concept here is the check occurs on the GET request, such that on the POST to save, Django will already have determined if this is a new or existing record.
If your check for an identical record is more complex, it might require shifting the logic around a bit.
I would do this -
if request.method == 'POST':
form = MyForm(request.POST)
if form.is_valid():
name = form.cleaned_data['name']
author = form.cleaned_data['author']
price = form.cleaned_data['prince']
if name and author and price:
book, created = Book.objects.get_or_create(name=name, \
author=author, price=price)
if created:
# fresh entry in db.
else:
# already there, maybe update?
book.save()
Based on the answers and comments, I had to create a different solution for my case, which included the use of unique_together on the base model. You may find this code useful as well, as I actually made it fairly generic.
I have custom code in the form.save() method that I want to utilize for creating a new object, so I don't want to simply not use the form.save() call. I do have to put my code check in the form.save() method, which I think is a reasonable place to put it.
I have a utility function to flatten iterables.
def flatten(l, a=list()):
"""
Flattens a list. Just do flatten(l).
Disregard the a since it is used in recursive calls.
"""
for i in l:
if isinstance(i, Iterable):
flatten_layout(i, a)
else:
a.append(i)
return a
In the ModelForm, I overwrite the validate_unique() method:
def validate_unique(self):
pass
This is about what my save method looks like:
def save(self, commit=True):
unique_fields = flatten(MyObject._meta.unique_together)
unique_cleaned_data = {k: v for k, v in self.cleaned_data.items() if k in unique_fields}
# check if the object exists in the database based on unique data
try:
my_object = MyObject.objects.get(**unique_cleaned_data)
except MyObject.DoesNotExist:
my_object = super(MyModelFormAjax, self).save(commit)
# -- insert extra code for saving a new object here ---
else:
for data, value in self.cleaned_data.items():
if data not in unique_fields:
# only update the field if it has data; otherwise, retain
# the old value; you may want to comment or remove this
# next line
if value:
setattr(my_object, data, value)
if commit:
my_object.save()
return my_object