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.
Related
I am trying to make one form for both inserting and updating data. I have read these:
Model validation on update in django
django exclude self from queryset for validation
In my project, however, I am not using ModelForm.
forms.py:
This is the form the user sees when registering his/her username and first_name. It is also the form an existing user sees when trying to change his/her username and/or first_name.
from django import forms
from .models import User
class SettingsForm(forms.Form):
username = forms.CharField(max_length=16)
first_name = forms.CharField(max_length=32)
# ... and many more form fields
def clean_slug(self):
"""Make sure that the username entered by the user will be unique in the database"""
username = self.cleaned_data['username']
try:
product = User.objects.get(username=username)
except User.DoesNotExist:
# Good, there is no one using this username
pass
else:
# There is alreaady a user with this username
raise forms.ValidationError('This username has been used. Try another.')
return username
The form cleaning works as intended for inserting data. However, when updating data, it complains that the username has been used (naturally, since the username already exists in the database).
How do I update the data without raising ValidationError when using a Form (and without using ModelForm)?
(The reasons for not using ModelForm in this case are: we may stop using the the orm, SettingsForm may contain a mix of fields from different models, some fields may be repeated hundreds of times in the form that is displayed to the user, we also need custom fields not tied to any model, ... and other scenarios that make using ModelForm quite challenging (impossible?). The purpose of this question is to find out if there are ways of achieving the desired result without using ModelForm.)
You have three cases:
The user is new
The user exists and doesn't change his/her username
The user exists and changes his/her username
You need to check if the username already exists only in the first two cases.
If it's an existing user you should pass the User instance to the form so you can use it in the clean_slug function, let's say in self.user variable.
Then you could just add two lines in the clean_slug function and it should work as you wish:
def clean_slug(self):
"""Make sure that the username entered by the user will be unique in the database"""
username = self.cleaned_data['username']
# If the user exists and the username has not been changed,
# just return the username
if self.user and self.user.username == username:
return username
try:
product = User.objects.get(username=username)
except User.DoesNotExist:
# Good, there is no one using this username
pass
else:
# There is alreaady a user with this username
raise forms.ValidationError('This username has been used. Try another.')
return username
The ValidationError is obviously because you're instantiating the SettingsForm when the username already exists, as you've already stated.
So if you want to add a form that can do double-duty, I would add an __init__ to SettingsForm that takes an is_update and saves it as a member variable, like so...
def __init__(self, is_update=False, **kwargs):
self.is_update = is_update
return super(SettingsForm, self).__init__(**kwargs)
then modify your clean_slug() to be:
def clean_slug(self):
username = self.cleaned_data['username']
try:
product = User.objects.get(username=username)
except User.DoesNotExist:
# Good, there is no one using this username
pass
else:
if not self.is_update: # for 'create' usage
# There is already a user with this username
raise forms.ValidationError('This username has been used. Try another.')
else: # for 'update' usage
pass
return username
You actually want your form to do two different things depending if it is a create or an update.
So either have two forms with a different clean_slug method or pass in an argument telling the form if it is an update or a create (there is another answer from neomanic showing this way).
Personally I think the easiest way would be to subclass your form and change the clean slug method. The use the new form for updates and your original form for creates.
class UpdateSettingsForm(settingsForm):
def clean_slug(self):
username = self.cleaned_data['username']
return username
I have models like this:
User:
#somefields
apps = models.ManyToManyField(App)
Here, User is created initially during registration. He links apps using many to many in a separate page where user selects(multiple) only apps, no other fields.
What is the best way to populate apps in Django ?
For example, if we can create User also, we can simply use modelform where .save(commit=False) will create user and .save_m2m() will link apps. But here we dont need to create User. We will be having user already, we only need to add apps. How ?
Newly added details:
I know how to do with normal "form" and normal web way. But I want to write quality code so want to know. If there is nothing wrong in using normal form then its fine.
There are multiple ways to do it, but for example:
class UserAppForm(ModelForm):
class Meta:
model = User
fields = ['apps']
And in your view, you configure the routing so that you can:
Get the ID of the user to select
Raise an error if you do not find the ID (Instead of creating a new one)
Initialize the form with the user
For example:
def user_add_app(request, pk):
user = get_object_or_404(User, pk=pk)
if request.method == "POST":
form = UserAppForm(request.POST, instance=user)
if form.is_valid():
user = form.save()
return HttpResponseRedirect(<some redirect>)
else:
form = EmployeeForm(instance=user)
return render(request, "<some html file>", {'form': form})
Maybe I did not fully understand the question (Please clarify if it's not the case), but there is no problem having two forms on the same model:
One form to create the User, without selecting apps (Which will be empty)
One form to add Apps to the User, taking into account that it already exists (And is not created at this time).
Does it answer the question ?
You can create a ModelForm form with model = App and a User field. Now when creating the form set an initial value for User :
form = AppForm(initial={'user': 1})
I have a form, where validation depends on logged user. For some users are certain values valid, for other users they are invalid. What is valid and what is invalid is dynamic - I can't create new form for each user group.
What's more I need this same validation in more forms, so I created custom form field. To this custom form field I need to pass user instance somehow to check if the value is valid or not.
How to do this?
I am doing it like that:
class EventForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
# doing stuff with the user…
super(EventForm, self).__init__(*args, **kwargs)
In your view class/method you have to instantiate the form like this:
form = EventForm(user=request.user, data=request.POST)
You don't need an extra field for this.
You can access the current user by passing it explicitly or by fetching it from the request in your form's init() method.
Then you can use the retrieved value when cleaning your form.
If you need this functionality in several forms I'd create either a base class that the specialized forms inherit from or create a mixin that adds the desired functionality.
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)
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.