Django-Userena: adding extra non-null fields to a user's profile - python

Is there an easy way to allow for required profile fields?
I am using userena in my current django project. I have a custom profile called UserProfile which has a start_year none-blank, non-null field as defined below.
class UserProfile(UserenaBaseProfile, PybbProfile):
user = models.OneToOneField(User, unique=True,
verbose_name=_('user'),
related_name='user_profile')
start_year = models.IntegerField(max_length=4)
I need this to be filled-in on signup. I created a SignupExtraForm as defined below, to override the default form.
class SignupFormExtra(SignupForm):
start_year = forms.IntegerField(label=_(u'Initiation Year'),
min_value=1800,
max_value=datetime.now().year,
required=True)
def save(self):
new_user = super(SignupFormExtra, self).save()
new_user_profile = new_user.get_profile()
new_user_profile.start_year = self.cleaned_data['start_year']
new_user_profile.save()
# Userena expects to get the new user from this form, so return the new
# user.
return new_user
When I attempt to add a new user thru the now modified form I get the below error:
profile_userprofile.start_year may not be NULL
With the stack trace pointing at new_user = super(SignupFormExtra, self).save()), in the code above.
I think this has to do with the user profile being created and saved before I am able to give it the required data from the form. Is there an easy way of supplying this data to the user_creation process, or delaying the creating of the user profile?
Thanks
Shon

UserProfile is created after the User is saved by a post_save signal. Even if you override it with your own signal, you won't have access to the form data from there.
The easiest solution is to just allow start_year to be NULL. It's not necessary to enforce this at the database level, and you can make the field required in all forms either way:
start_year = models.IntegerField(max_length=4, blank=False, null=True)
Your custom form already enforces that the field is required, so you're done.
UPDATE (from comment)
Custom form, yes, but you can still use a ModelForm:
class MyModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
self.fields['start_year'].required = True
UPDATE (again)
Actually, I didn't think before my last update. I put blank=False on the start_year field in my example. That will force that the field is required in all ModelForms by default. You don't need a custom ModelForm at all. However, I left the previous update for posterity.

Related

Django Model: How to only access fields related to a specific user from another model

How can i only access the addresses(Address model) of specified user(User model) from Order model.
here is the code: Models.py
class User(AbstractBaseUser, PermissionsMixin):
phone_number = PhoneField(max_length=12, primary_key=True, unique=True)
class Address(models.Model):
address = models.CharField(max_length=500, blank=False,null=False,primary_key=True)
customer = models.ForeignKey((User, on_delete= models.CASCADE)
class Order(models.Model):
order = CharField(max_length=400,blank=False,null=False)
customer = models.ForeignKey(User,on_delete=models.SET_NULL, null=True)
address = models.ForeignKey(Address,on_delete=models.SET_NULL, null=True)
the address field in Order model is my problem. When creating a new order in Django Administration, when i select one of the customers, i still can choose any address registered in database
How can i limit the access to addresses to specified user.
Thanks in advance
You can not filter this in the models. You will need to do that by the form layer.
We can implement this with:
class MyForm(forms.ModelForm):
def __init__(self, *args, user=None, **kwargs):
super().__init__(*args, **kwargs)
self.fields['address'].queryset = Address.objects.filter(user=user)
class Meta:
model = Order
fields = ['address']
then in the view, we can construct a form with the logged in user as user:
from django.contrib.auth.decorators import login_required
#login_required
def my_view(request):
if request.method == 'POST':
form = MyForm(request.POST, user=request.user)
if form.is_valid():
form.instance.user = request.user
# set the order number to the instance
form.save()
return redirect('name-of-some-view')
else:
form = MyForm(user=request.user)
return render(request, 'name-of-some-template.html', {'form': form})
It depends if customers should be allowed to have multiple addresses (like in most online shops). In that case the User and Address models look pretty good!
Django Admin can be a bit tricky. But that's just the nature of the flow, because the moment you open the "Create Order Page" the server has no idea what user you will pick, and therefore does not know which address it should filter. You would have to use ajax to get to your goal, but I can propose something different...
The question is why did you add another address field to the Order? Don't get me wrong, it's the right way actually. But...
What if the user orders something, changes his address object and looks back at the order history?
Actually you COULD drop the address-foreignkey on the order and you'll still be able to access the current customer address on any order, by:
some_order = Order.objects.first()
customer = some_order.customer
# As defined in your model, one customer can have many addresses. For now just access the "latest" one
customers_address = customer.address_set.last()
But the order history would still be messy... now its even worse. Whenever the customer adds or changes the address, the order history would show wrong values.
To prevent this, you could leave the foreign key, prevent the address_id from being edited (read_only field), prevent the related address object from being edited, and add a flag if the address is visible to the user or soft-deleted.
You should do some research about read_only fields, editable and overriding model methods
But to keep things a bit more simple, lets just change the Order->address field to be a Charfield instead of a foreign key. You won't need to show an editable field inside the admin anymore and instead let the user have his default address.
class Order(models.Model):
order = CharField(max_length=400,blank=False,null=False)
customer = models.ForeignKey(User,on_delete=models.SET_NULL, null=True)
shipping_address = models.CharField(max_length=500, editable=False)
def save(self, *args, **kwargs):
# You can use this field to stringify even more complex objects
# Again, last() is not the right way in the end but you could have a specific field on the customer: preferred_address
self.shipping_address = self.customer.address_set.last().address
super().save(*args, **kwargs)

What is the correct way to auto-create related model objects in django-registration?

I'm a total Django newbie and apologize in advance if I'm not using the correct terminology.
I'm using django-registration to register user on my web-app. I've successfully adapted it work with my custom user model GeneralUser.
Conceptually, each GeneralUser has a Business, another model that I've defined. Whether this is the correct decision or not, I've decided that I never want to register a user without a related Business object.
I've read countless threads on customizing django form, and finally after a few days of unsuccessful attempts, came upon an answer that helped reach a solution. However, I am unsure that my code is correct/safe. This is my adaptation, followed by the linked-answer:
my adaptation:
class GeneralUserForm(UserCreationForm):
business_name = forms.CharField(required=True)
class Meta:
model = GeneralUser
fields = ['username', 'email', 'password1',
'password2', 'business_name']
def save(self, commit=True):
user = super(UserCreationForm, self).save(commit=True)
business = Business(name=user.business_name, owner=user)
# notice: no if-block
user.save()
business.save()
# notice: returning only a user-instance
return user
This code successfully creates a user and a business object, and creates the relationship. Looking at the original answer code though, I wonder if there isn't something critical I'm missing:
Answer I based my code on:
class UserCreateForm(UserCreationForm):
job_title = forms.CharField(max_length=100, required=True)
age = forms.IntegerField(required=True)
class Meta:
model = User
def save(self, commit=True):
if not commit:
raise NotImplementedError("Can't create User and UserProfile without database save")
user = super(UserCreateForm, self).save(commit=True)
user_profile = UserProfile(user=user, job_title=self.cleaned_data['job_title'],
age=self.cleaned_data['age'])
user_profile.save()
# notice: multiple returns
return user, user_profile
A few questions about the differences:
Why doesn't my code work if I end it like this:
.
if commit:
user.save()
business.save()
return user
I'm not using cleaned_data, is that okay?
What is the purpose of the if not commit block in the original code?
Most importantly, is this a "legitimate way" to handle user registration that requires an automatic object-relation on creation?
cleaned_data is the dictionary of data after all validation in every field in that form. Now you can decide whether to rely on it or not(preferably you should).So as a pseudocode we could say cleaned_data + errors will be all fields.
commit is used to decide whether it should commit to db(write). From the above code, to add a related model object like profile, the original object(User) has to be created first.Thats why it make force commit.
To add a related object on object creation, there are multiple ways like post_save signals, override model save, override form save etc.So you are using one of good approach, I would say.

How can I init ManyToMany field in django models that can't relate to itself(object level)?

Example:
class MyUser(models.Model):
blocked_users = models.ManyToManyField("self", blank=True, null=True)
user = MyUser.object.get(pk=1)
user.blocked_users.add(user)
user.blocked_users.all()[0] == user # (!!!)
Can It be prevented on model/db level? Or we need just do check somewhere in app.
Looking at the Django docs for ManyToManyField arguments, it does not seem possible.
The closest argument to what you want is the limit_choices_to However, that only limits choices on ModelForms and admin (you can still save it like you did in your example), and there is currently no easy way to use it to limit based on another value (pk) in the current model.
If you want to prevent it from happening altogether, you'll have to resort to overriding the save method on the through model--something like:
class MyUser(models.Model):
blocked_users = models.ManyToManyField(..., through="BlockedUser")
class BlockedUser(models.Model):
user = models.ForeignKey(MyUser)
blocked = models.ForeignKey(MyUser)
def save(self, *args, **kwargs):
# Only allow this relationship to be created if
if self.user != self.blocked:
super(BlockedUser, self).save(*args, **kwargs)
You could of course also do this with signals.

Django: UpdateView with unique field

I'm trying to create an update profile page for my custom User model. In my model, my email field is set to be unique.
class User(UserBase):
...
email = models.EmailField(
max_length=100,
unique=True,
blank=True,
verbose_name='email address',
)
Then in my view I have:
class UpdateProfileView(LoginRequiredMixin, UpdateView):
template_name = 'accounts/update-profile.html'
form_class = UpdateProfileForm
model = User
The only thing that UpdateProfileForm does is check that the old password is different from the new one in the clean method.
My issue is that when I save the form I'm getting the error message User with this Email address already exists.. Since it's an update view and saving a unique field that hasn't changed shouldn't it not throw this error? If this is the correct behavior, then how do I save the form and ignore the email address if it hasn't changed.
Thanks for the help in advance.
Remove blank=True from your User Model definition. The field definition is null=False by default and additionally you specify the field must be unique—it's a important field—so you don't want your form validation to allow blank values. Here is the Django documentation on those attributes. blank is entirely a form validation thing. That alone might fix the error.
Unless you have custom form logic/validation, you don't need the form_class attribute on your UpdateProfileView. From the docs: "These generic views will automatically create a ModelForm". (There is even an UpdateView example).
See if the view works without form_class and if it does then examine your UpdateProfileForm code.
Here are some suggestions/alternatives:
If you stop using Generic Views (i.e. UpdateProfileView), you can then do the following steps in your logic view: if the request is a POST, take the data from the form and update your Model (How to update fields in a model without creating a new record in django?)
Why don't you use a ModelForm instead? https://docs.djangoproject.com/en/dev/topics/forms/modelforms/
Why don't you work with User from django.contrib.auth.models? https://docs.djangoproject.com/en/dev/topics/auth/default/#user-objects
Finally, have you considered working with this already built Django registration app? http://www.michelepasin.org/blog/2011/01/14/setting-up-django-registration/
Never use blank=True and unique=True, its senseless. If you want to make this field is not required in form, just do.
class Form(forms.Form):
...
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['unique_field].required = False
In addition to previous answer, when u use blank=True and unique=True, the "Is already exits ... blah bla" its correct behavior, coz form accepting empty string as value and its already exists. You need to override clean_field method:
class Form(forms.Form):
...
def clean_unique_id(self):
"""
Take new value or if its '' e.g. None take initial value
"""
return self.data['unique_id'] or self.initial['unique_id']

Set field value in Django Form clean() method, if this field not passed in constructor

I need set field value, not passed to Django Form constructor.
I have model and form like this:
class Message(models.Model):
created = models.DateTimeField()
text = models.CharField(max_length=200, blank=True, null=True)
active = models.BooleanField(default=False)
class MessageForm(forms.ModelForm):
class Meta:
model = Message
exclude = ('created', 'active')
def clean(self):
# check if user is blocked
if user.is_admin():
self.cleaned_data['active'] = True
return self.cleaned_data
Expected: if current user is admin - I need automatically set message as active. User should not pass this parameter by form.
Actual: I see that saved message always have flag "False" (I can delete condition and in this case I also see that message is not active).
Please help me understand, how can I do set this "active" flag in clean() method.
The previous answer would work, but I like encapsulating all the form's internal operations like what to show and what not, within the form. I know you mentioned you don't want to send a field value to the constructor, but if you don't mind sending the user, your solution would work.
i.e., your constructor:
def __init__(self, user):
self.user = user
super(BaseForm, self).__init__()
then in your clean, you just change the user to self.user.
There is another added benefit to this. Say tomorrow you want to assign more fields based on your user, you don't need to add anything to the views, you can simply add it to the form.
EDIT:
When you add a field to exclude, it is not available in the cleaned data. Instead, set its widget as hidden.
active = forms.BooleanField(widget=forms.HiddenInput)
EDIT 2: If you really don't want the field in the form
In this case, instead of overriding the clean, why don't you override the save?
def save (self):
super(BaseForm, self).save()
if user.is_admin():
self.instance.active=True
super(BaseForm, self).save()
Don't do this in the form's clean() method, do this in the view.
def your_view(request):
if request.method == 'POST':
form = MessageForm(data=request.POST)
if form.is_valid():
new_message = form.save(commit=False)
if user.is_admin():
new_message.active = True
However, if you also want to handle the case where your user is not admin using the same form, you can look at incorporating similar logic in the form's init() instead of the view, probably by passing info about the user from the view to the form's init()
Use this:
def message_form_factory(user):
class MessageForm(forms.ModelForm):
def clean(self):
# check if user is blocked
if user.is_admin():
self.cleaned_data['active'] = True
return self.cleaned_data
return MessageForm
And in your view use:
form = message_form_factory(request.user)()
form = message_form_factory(request.user)(request.POST)

Categories