33

I have a model, called Student, which has some fields, and a OneToOne relationship with a user (django.contrib.auth.User).

class Student(models.Model):

    phone = models.CharField(max_length = 25 )
    birthdate = models.DateField(null=True) 
    gender = models.CharField(max_length=1,choices = GENDER_CHOICES) 
    city = models.CharField(max_length = 50)
    personalInfo = models.TextField()
    user = models.OneToOneField(User,unique=True)

Then, I have a ModelForm for that model

class StudentForm (forms.ModelForm):
    class Meta:
        model = Student

Using the fields attribute in class Meta, I've managed to show only some fields in a template. However, can I indicate which user fields to show?

Something as:

   fields =('personalInfo','user.username')

is currently not showing anything. Works with only StudentFields though/

Thanks in advance.

John Moutafis
  • 22,254
  • 11
  • 68
  • 112
Tom
  • 43,810
  • 29
  • 138
  • 169
  • if Student model inherited the User model you would only need one modelform. – Kevin L. Jan 26 '18 at 10:27
  • @KevinL., it would be great if you elaborate this in an answer :-) – cel Jan 27 '18 at 07:41
  • @cel AFAIK, there have been no such developments like this to speak of in the Django core. An 'automatic' solution here would likely be non-trivial and involve writing your own custom modelform classes or mixins to do so. This would be significantly more complex (and arguably more fragile) than using the suggested methods. One possible solution might be contained in [this answer](https://stackoverflow.com/a/41559015/5747944) which describes a ModelForm mixin that allows defining a second 'child' model and is claimed to be compatible with generic views. – sytech Jan 31 '18 at 14:20
  • Hey, @Tom, I was wondering, did you find any answer helpful? – John Moutafis Apr 03 '18 at 07:33
  • I'm not sure this is what you want, but you can take a look at the [documentation for Inline Formsets](http://docs.djangoproject.com/en/dev/topics/forms/modelforms/#inline-formsets) – Flávio Amieiro Sep 27 '09 at 17:04

4 Answers4

32

A common practice is to use 2 forms to achieve your goal.

  • A form for the User Model:

    class UserForm(forms.ModelForm):
        ... Do stuff if necessary ...
        class Meta:
            model = User
            fields = ('the_fields', 'you_want')
    
  • A form for the Student Model:

    class StudentForm (forms.ModelForm):
        ... Do other stuff if necessary ...
        class Meta:
            model = Student
            fields = ('the_fields', 'you_want')
    
  • Use both those forms in your view (example of usage):

    def register(request):
        if request.method == 'POST':
            user_form = UserForm(request.POST)
            student_form = StudentForm(request.POST)
            if user_form.is_valid() and student_form.is_valid():
                user_form.save()
                student_form.save()
    
  • Render the forms together in your template:

    <form action="." method="post">
        {% csrf_token %}
        {{ user_form.as_p }}
        {{ student_form.as_p }}
        <input type="submit" value="Submit">
    </form>
    

Another option would be for you to change the relationship from OneToOne to ForeignKey (this completely depends on you and I just mention it, not recommend it) and use the inline_formsets to achieve the desired outcome.

John Moutafis
  • 22,254
  • 11
  • 68
  • 112
  • 1
    @cel I don't know about an "automatic" way (as you put it) but I have a manual solution in this answer. Have a look :) – John Moutafis Jan 26 '18 at 09:49
8

Both answers are correct: Inline Formsets make doing this easy.

Be aware, however, that the inline can only go one way: from the model that has the foreign key in it. Without having primary keys in both (bad, since you could then have A -> B and then B -> A2), you cannot have the inline formset in the related_to model.

For instance, if you have a UserProfile class, and want to be able to have these, when shown, have the User object that is related shown as in inline, you will be out of luck.

You can have custom fields on a ModelForm, and use this as a more flexible way, but be aware that it is no longer 'automatic' like a standard ModelForm/inline formset.

Matthew Schinckel
  • 35,041
  • 6
  • 86
  • 121
  • 2
    I am hitting exactly the problem described in this question and your answer. Now many years have passed. Do you know whether there is an automatic solution now? – cel Jan 26 '18 at 07:06
4

An alternative method that you could consider is to create a custom user model by extending the AbstractUser or AbstractBaseUser models rather than using a one-to-one link with a Profile model (in this case the Student model). This would create a single extended User model that you can use to create a single ModelForm.

For instance one way to do this would be to extend the AbstractUser model:

from django.contrib.auth.models import AbstractUser

class Student(AbstractUser):

    phone = models.CharField(max_length = 25 )
    birthdate = models.DateField(null=True) 
    gender = models.CharField(max_length=1,choices = GENDER_CHOICES) 
    city = models.CharField(max_length = 50)
    personalInfo = models.TextField()
    # user = models.OneToOneField(User,unique=True)   <= no longer required

In settings.py file, update the AUTH_USER_MODEL

AUTH_USER_MODEL = 'appname.models.Student'

update the model in your Admin:

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import Student

admin.site.register(Student, UserAdmin)

Then you can use a single ModelForm that has both the additional fields you require as well as the fields in the original User model. In forms.py

from .models import Student    

class StudentForm (forms.ModelForm):
    class Meta:
        model = Student
        fields = ['personalInfo', 'username']

A more complicated way would be to extend the AbstractBaseUser, this is described in detail in the docs.

However, I'm not sure whether creating a custom user model this way in order to have a convenient single ModelForm makes sense for your use case. This is a design decision you have to make since creating custom User models can be a tricky exercise.

Kevin L.
  • 1,371
  • 11
  • 24
1

From my understanding you want to update the username field of auth.User which is OneToOne relation with Student, this is what I would do...

class StudentForm (forms.ModelForm):

username = forms.Charfield(label=_('Username'))
class Meta:
    model = Student
    fields = ('personalInfo',)

def clean_username(self):
    # using clean method change the value
    # you can put your logic here to update auth.User
    username = self.cleaned_data('username')
    # get AUTH USER MODEL
    in_db = get_user_model()._default_manager.update_or_create(username=username)

hope this helps :)

Sanchit
  • 326
  • 3
  • 6