Django update a model form - django

I have a modelform:
class UserPreferencesForm(ModelForm):
"""
Form for storing user preferences.
"""
class Meta:
model = UserPreferences
exclude = ('user')
the model:
class UserPreferences(models.Model):
"""
Model for user project preferences.
"""
user = models.OneToOneField(User)
...
and in the views:
...
form = UserPreferencesForm(request.POST or None)
if form.is_valid():
# save the form
prefs = form.save(commit=False)
prefs.user = request.user
prefs.update()
messages.add_message(
request, messages.INFO, 'Your preferences have been updated.'
)
...
I want to ensure that each user only has one set of preferences, so I would like to refactor the view code to use something along the lines of the update() model method instead of checking for object existence and then saving, which would incur more queries.
What is the most efficient way of 'create-or-updating' the model?
Any help much appreciated.

Are you interested in saving the query to detect if a row exists?
In the case, you could do as you describe.. do an update and check if 0 rows were updated, which implies the profile doesn't exist.
updated = Preferences.objects.filter(user=request.user).update(**form.cleaned_data)
if updated == 0:
# create preference object
But an even simpler design pattern is to ensure there is always a preferences table for every user via a signal listening on models.signals.post_save sent by the User class.
Then, you can assume it always exists.
def create_prefs(sender, instance, created, **kwargs):
if created:
# create prefs
models.signals.post_save.connect(create_prefs, sender=User)

Related

Django: Update object from view, with no model or template

I'm currently working on a Django app that allows users to set synonyms for keywords.
Its using the following model:
class UserSynonym(models.Model):
user = models.ForeignKey(Tenant)
key = models.CharField(max_length=255)
value = models.CharField(max_length=255)
A ListView provides users with an overview, and each entry is followed by an 'edit' button that links to a relevant
EditView that allows the user to change the 'value' parameter.
I now want to add a button that allows the user to quickly reset the object to its original meaning (Set the 'value' to the value of 'key')
I want this to be a single button, without any forms or html templates. Just a button on the list that, when pressed, 'resets' the value of the model.
I reckon I have to edit an EditView for this task, however, these views keep demaning that I supply them with either a form or a HTML template/page
Which is not what I want to do.
Is there a way to change the EditView so that it changes the value of the object, without redirecting the user to a form or a new webpage?
EDIT:
for completeness sake I've added the UpdateView as I'm currently using it
class SynonymUpdate(UserRootedMixin, UpdateView):
model = UserSynonym
form_class = SynonymCreateForm
def get_form_kwargs(self):
kwargs = super(SynonymUpdate, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
form.instance.linked_user = self.kwargs.get('user')
return super(SynonymUpdate, self).form_valid(form)
def get_success_url(self, **kwargs):
return reverse('synonym_list', args=[self.request.user.id])
I sort of solved my problem. I gave up on using a class based view and used the following function instead:
def SynonymReset(request, user_id, pk):
"""(re)sets the Synonym.value to the value of Synonym.key"""
#Get relevant variables
currentuser = User.objects.get(id=request.user.id)
currentsynonym = Synonym.objects.get(id = pk)
#(re)set object & save
currentsynonym.value = currentsynonym.key
currentsynonym.save()
#Return to the listview.
return redirect('synonym_list', user=current_user)
This way the value is reset, without going to a seperate webpage. I still hope to one day find out how to do this in a class based view. But for now this will suffice.

Check previous model value on save

I have a model that saves an Excursion. The user can change this excursion, but I need to know what the excursion was before he change it, because I keep track of how many "bookings" are made per excursion, and if you change your excursion, I need to remove one booking from the previous excursion.
Im not entirely sure how this should be done.
Im guessing you use a signal for this?
Should I use pre_save, pre_init or what would be the best for this?
pre_save is not the correct one it seems, as it prints the new values, not the "old value" as I expected
#receiver(pre_save, sender=Delegate)
def my_callback(sender, instance, *args, **kwargs):
print instance.excursion
Do you have several options.
First one is to overwrite save method:
#Delegate
def save(self, *args, **kwargs):
if self.pk:
previous_excursion = Delegate.objects.get(self.pk).excursion
super(Model, self).save(*args, **kwargs)
if self.pk and self.excursion != previous_excursion:
#change booking
Second one is binding function to post save signal + django model utils field tracker:
#receiver(post_save, sender=Delegate)
def create_change_booking(sender,instance, signal, created, **kwargs):
if created:
previous_excursion = get it from django model utils field tracker
#change booking
And another solution is in pre_save as you are running:
#receiver(pre_save, sender=Delegate)
def my_callback(sender, instance, *args, **kwargs):
previous_excursion = Delegate.objects.get(self.pk).excursion
if instance.pk and instance.excursion != previous_excursion:
#change booking
You can use django model utils to track django model fields. check this example.
pip install django-model-utils
Then you can define your model and use fieldtracker in your model .
from django.db import models
from model_utils import FieldTracker
class Post(models.Model):
title = models.CharField(max_length=100)
body = models.TextField()
tracker = FieldTracker()
status = models.CharField(choices=STATUS, default=STATUS.draft, max_length=20)
after that in post save you can use like this :
#receiver(post_save, sender=Post)
def my_callback(sender, instance,*args, **kwargs):
print (instance.title)
print (instance.tracker.previous('title'))
print (instance.status)
print (instance.tracker.previous('status'))
This will help you a lot to do activity on status change. as because overwrite save method is not good idea.
As an alternative and if you are using Django forms:
The to-be version of your instance is stored in form.instance of the Django form of your model. On save, validations are run and this new version is applied to the model and then the model is saved.
Meaning that you can check differences between the new and the old version by comparing form.instance to the current model.
This is what happens when the Django Admin's save_model method is called. (See contrib/admin/options.py)
If you can make use of Django forms, this is the most Djangothic way to go, I'd say.
This is the essence on using the Django form for handling data changes:
form = ModelForm(request.POST, request.FILES, instance=obj)
new_object = form.instance # not saved yet
# changes are stored in form.changed_data
new_saved_object = form.save()
form.changed_data will contain the changed fields which means that it is empty if there are no changes.
There's yet another option:
Django's documentation has an example showing exactly how you could do this by overriding model methods.
In short:
override Model.from_db() to add a dynamic attribute containing the original values
override the Model.save() method to compare the new values against the originals
This has the advantage that it does not require an additional database query.

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 form validation with authenticated user as a field

Model:
class ProjectType(models.Model):
project_type_id = models.AutoField(primary_key=True)
name = models.CharField(max_length=45, help_text='Type of project', verbose_name='Project Type')
slug = models.SlugField(max_length=45, blank=True)
description = models.CharField(max_length=400, help_text='Description of the main purpose of the project', verbose_name='Project Type Description')
default = models.BooleanField(default=False)
owner = models.ForeignKey(User)
class Meta:
...
unique_together = (('slug', 'owner'),('name', 'owner'))
I need a form to create/update ProjectType's. Please note the owner field - it is supposed to be current logged-in user. The question is how to ensure that constraints in the unique_together are validated correctly.
I do not want to show owner field on the form - it's the current user, so it should be set automatically by the system. But no matter how I try to do this, either validation does not work, or there are other errors.
Among approaches I tried (individually or in combination):
Creating a hidden field in the related ModelField
Defining init in ProjectTypeForm (in various ways), for example:
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super(ProjectTypeForm, self).__init__(*args, **kwargs)
self.fields['owner'].initial = self.user
Setting values in the view like:
...
if request.method == 'POST':
project_type = ProjectType(owner=request.user)
form = ProjectTypeForm(request.POST, instance=project_type, user = request.user.pk) # also tries w/o pk
...
Overriding clean() method of the form in various ways, along these lines:
def clean(self):
cleaned_data = super(ProjectTypeForm, self).clean()
slug=cleaned_data.get('slug')
owner = cleaned_data.get('owner')
if slug:
user = User.objects.get(pk=owner)
...
Many of these approaches are based on various answers found on stackoverflow.com. However, no matter what I try, I cannot find a way to accomplish what I need: (1) auto-setting of the owner field and (2) validation for uniqueness: owner/type_name and owner/type_slug. Typical errors I have is that (a) owner is not recognized as a User (it's treated as a PK), (b) incorrect validation (like lack of it or it misses the fact that it's the same record being edited, etc.), (c) owner is a required field.
For the record - if the owner is a regular field in the form, everything works as expected, but I cannot allow users to set the owner value.
Is there any, hopefully elegant, solution to this?
Thanks!
Exclude the owner field from your form, and save the user in your form's init method - then you can use it to validate the form, eg
class ProjectTypeForm(...):
...
def __init__(self, user, *args, **kwargs):
super(ProjectTypeForm, self).__init__(*args, **kwargs)
self.user = user
def clean(self):
user_projects = ProjectType.objects.filter(owner=self.user)
if user_projects.filter(slug=self.cleaned_data['slug']):
raise forms.ValidationError('...')
elif user_projects.filter(name=self.cleaned_data['name']):
raise forms.ValidationError('...')
else:
return self.cleaned_data
Then in your view, do something like this when creating a new ProjectType:
if request.method == 'POST':
form = ProjectTypeForm(request.user, request.POST)
if form.is_valid():
ptype = form.save(commit=False)
ptype.owner = request.user
ptype.save()
You shouldn't need that to save existing ProjectType objects though.
As I mentioned in my comment, one possible solution is essentially to go along with Django forms and use the owner field on the form. So, what I've done is modified init in this way:
def __init__(self, user, *args, **kwargs):
super(ProjectTypeForm, self).__init__(*args, **kwargs)
self.fields['owner'] = forms.ModelChoiceField(
label='Owner*',
queryset=User.objects.filter(username=user.username),
help_text="Project types are unique to logged-in users who are set as their owners.",
required=True,
empty_label=None)
Basically, what it does it is still using ChoiceField but sets it to one option - current user. In addition, empty_label=None ensures that there is no "empty" choice. The effect is (since username is unique) that current user name appears visible and is the only choice in the otherwise dropdown list with more choices.
In the view I follow this approach:
...
if request.method == 'POST':
project_type = ProjectType()
form = ProjectTypeForm(request.user,request.POST, instance=project_type,)
if form.is_valid():
project_type.save()
return HttpResponseRedirect(reverse('project_types'))
else:
form = ProjectTypeForm(request.user)
...
Basically, that's it - validation of unique constraints (and the whole thing) works like a charm.
Do I like this solution? No. I consider it a hack (ironically, even if it goes along with standard Django approaches). But it requires something that is totally unnecessary. One benefit of this approach is that it clearly communicates to the current user that s/he is set as the project type owner. But even with this in mind I would rather show a message (instead of a field) that Current user X will be set as the owner of the project type being created. So, if someone has a better solution, please submit it to illustrate the full power and flexibility of Django.

Allow changing of User fields (like email) with django-profiles

Django lets you create a model foreign-keyed to User and define it in settings as the official "profile" model holding additional data for user accounts. django-profiles lets you easily display/create/edit that profile data. But the user's primary email address is part of their main account, not part of their extended profile. Therefore when you put
{{ form }}
in the profile/edit_profile template, the primary email address does not show up. You can retrieve it manually with
{{ user.email }}
but changes to it aren't saved back to the account upon submit of course. I'm assuming a custom ModelForm has been created, such as:
class ProfileForm(ModelForm):
class Meta:
model = Parent
exclude = ('family','user','board_pos','comm_job',)
and that ProfileForm is being passed to django-profiles' view code with a urlconf like:
('^profiles/edit', 'profiles.views.edit_profile', {'form_class': ProfileForm,}),
The same problem would come up if you wanted to let users change their first or last names. What's the best way to let users change their own email addresses or names when using django-profiles?
Here's the solution we ended up using:
# urls.py
# First match /profiles/edit before django-profiles gets it so we can pass in our custom form object.
('^profiles/edit', 'profiles.views.edit_profile', {'form_class': ProfileForm,}),
(r'^profiles/', include('profiles.urls')),
Now we override the save method in the form itself, so that when the form is saved, the email address is pushed into the saving user's User object at the same time. Graceful.
# forms.py , trimmed for brevity
class ProfileForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ProfileForm, self).__init__(*args, **kwargs)
try:
self.fields['email'].initial = self.instance.user.email
except User.DoesNotExist:
pass
email = forms.EmailField(label="Primary email")
class Meta:
model = Parent
def save(self, *args, **kwargs):
"""
Update the primary email address on the related User object as well.
"""
u = self.instance.user
u.email = self.cleaned_data['email']
u.save()
profile = super(ProfileForm, self).save(*args,**kwargs)
return profile
Works perfectly. Thanks mandric.
I think that implementing a Separate page just for change of email is best, since it would need to be verified etc...
If you would like to enable users to modify all their profile info together with their main email address, then you need to create your own Form (ModelForm will not work here). I suggest you start doing this and post a question when you get stuck.
Start by copying all the fields out of django-profile model into your custom form, and add the users primary email field.
You will have to "override" the django-profile edit url and basically copy the html template if there is one.
Another option (bad) would be to hack django-profiles app and change it there. But that will, likely, introduce a lot of bugs, and will render your app unapgradable.
I think the easiest way would definitely be to use a form. Use the form to display their current email address (which they could change), and then use your view to extract the request, retrieve the appropriate profile belonging to that user by matching some other parameter you could pass to the template, and then storing the new data and saving the model.