I have a custom user model where I handle three user profiles: StudentProfile
, ProfessorProfile
and ExecutiveProfile
. Each of these models is related to my User
custom model
For these three profiles, I have three forms that inherit from form.ModelForms
, where the fields of each user profile are generated:
class StudentProfileForm(forms.ModelForm):
class Meta:
model = StudentProfile
fields = ('origin_education_school', 'current_education_school',
'extra_occupation')
class ProfessorProfileForm(forms.ModelForm):
class Meta:
model = ProfessorProfile
fields = ('occupation',)
class ExecutiveProfileForm(forms.ModelForm):
class Meta:
model = ExecutiveProfile
fields = ('occupation', 'enterprise_name', 'culturals_arthistic','ecological')
To access this user profile view, I have created this URL
url(r"^profile/(?P<slug>[\w\-]+)/$",
views.AccountProfilesView.as_view(),
name='profile'
),
Additionally, my view AccountProfilesView
has the following behavior:
From the class-based view AccountProfilesView
, each of these forms is being instantiated, as the user's profile corresponds, so that if a user has the profile is_student
, its corresponding form will be generated and so on for profiles is_professor
e is_executive
.
If a user has the three profiles ( is_student
, is_professor
e is_executive
) the fields of the three forms will be created for their corresponding data associated with these profiles.
class AccountProfilesView(LoginRequiredMixin, UpdateView):
# All users can access this view
model = get_user_model()
success_url = reverse_lazy('dashboard')
template_name = 'accounts/profile_form.html'
fields = '__all__'
def get_context_data(self, **kwargs):
context = super(AccountProfilesView, self).get_context_data(**kwargs)
user = self.request.user
if not self.request.POST:
if user.is_student:
profile = user.get_student_profile()
context['userprofile'] = profile
context['form_student'] = forms.StudentProfileForm()
if user.is_professor:
profile = user.get_professor_profile()
context['userprofile'] = profile
context['form_professor'] = forms.ProfessorProfileForm()
# print ("profesor form is", context['form_professor'])
if user.is_executive:
profile = user.get_executive_profile()
context['userprofile'] = profile
context['form_executive'] = forms.ExecutiveProfileForm()
else:
if user.is_student:
context['form_student'] = forms.StudentProfileForm(
self.request.POST)
if user.is_professor:
context['form_professor'] = forms.ProfessorProfileForm(
self.request.POST)
if user.is_executive:
context['form_executive'] = forms.ExecutiveProfileForm(
self.request.POST)
return context
def form_valid(self, form):
context = self.get_context_data(form=form)
user = self.request.user
user = form.save()
if user.is_student:
student = context['form_student'].save(commit=False)
student.user = user
student.save()
if user.is_professor:
professor = context['form_professor'].save(commit=False)
professor.user = user
professor.save()
if user.is_executive:
executive = context['form_executive'].save(commit=False)
executive.user = user
executive.save()
return super(AccountProfilesView, self).form_valid(form)
And in my template, I have the following little logic:
<form method="POST">
{% csrf_token %}
{% if userprofile.user.is_student %}
<div align="center"><i>My Student Profile data</i></div>
{% bootstrap_form form_student %}
{% endif %}
{% if userprofile.user.is_professor %}
<div align="center"><i>My Professor Profile data</i></div>
{% bootstrap_form form_professor %}
{% endif %}
{% if userprofile.user.is_executive %}
<div align="center"><i>My Executive Profile data</i></div>
{% bootstrap_form form_executive %}
{% endif %}
<input type="submit" value="Save Changes" class="btn btn-default">
</form>
The forms are displayed according to the user's profile, but what happens to me is that the forms don't save anything to the database when I submit... and besides, success_url
it behaves all crazy because it doesn't redirect me to the URL that I want is the dashboard/
, but it stays in that same template, but I lose the action of being logged in with my user, that is, it leaves me without action to the LoginRequiredMixin().
In it, the overwriting that I perform of the method form_valid()
is where I have the instruction to record the data of the corresponding forms.
Is there something wrong with the method override form_valid()
?
UPDATE
Following the answer given by @German-Alzate and his recommendation to use a function-based view, I would like to complement some details of my scenario and ask some questions that arise from its given implementation:
In my custom Users model I have the methods get_student_profile()
, get_professor_profile()
and get_executive_profile()
to obtain the data of each user in my views. They are as follows:
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
username = models.CharField(max_length=40, unique=True)
slug = models.SlugField(max_length=100, blank=True)
is_student = models.BooleanField(default=False)
is_professor = models.BooleanField(default=False)
is_executive = models.BooleanField(default=False)
def get_student_profile(self):
student_profile = None
if hasattr(self, 'studentprofile'):
student_profile = self.studentprofile
return student_profile
def get_professor_profile(self):
professor_profile = None
if hasattr(self, 'professorprofile'):
professor_profile = self.professorprofile
return professor_profile
def get_executive_profile(self):
executive_profile = None
if hasattr(self, 'executiveprofile'):
executive_profile = self.executiveprofile
return executive_profile
Addition to update
When I create a user with any profile is_student
, is_professor
I is_executive
have a signal that automatically takes the value of the field username
and saves it in the slug
model fieldUser
@receiver(post_save, sender=User)
def post_save_user(sender, instance, **kwargs):
slug = slugify(instance.username)
User.objects.filter(pk=instance.pk).update(slug=slug)
In this way I use that field slug
to show it in the preferences and profile url of each user.
And each user profile has its own model-related model User
, plus a slug field, which takes the value username
of its own model-related user User
.
It is in this way that in the model User
, I override the method save()
to indicate that the field username
of a user that is being created, is equal in its value to the field slug
of the profile that that user will take ( StudentProfile
, ProfessorProfile
, ExecutiveProfile
):
def save(self, *args, **kwargs):
user = super(User,self).save(*args,**kwargs)
# Creating an user with student, professor and executive profiles
if self.is_student and not StudentProfile.objects.filter(user=self).exists() \
and self.is_professor and not ProfessorProfile.objects.filter(user=self).exists() \
and self.is_executive and not ExecutiveProfile.objects.filter(user=self).exists():
student_profile = StudentProfile(user=self)
student_slug = self.username
student_profile.slug = student_slug
professor_profile = ProfessorProfile(user=self)
professor_slug = self.username
professor_profile.slug = professor_slug
executive_profile = ExecutiveProfile(user=self)
executive_slug = self.username
executive_profile.slug = executive_slug
student_profile.save()
professor_profile.save()
executive_profile.save()
And the models of each profile ( StudentProfile
, ProfessorProfile
, ExecutiveProfile
): are:
class StudentProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
slug = models.SlugField(max_length=100,blank=True)
origin_education_school = models.CharField(max_length=128)
current_education_school = models.CharField(max_length=128)
extra_occupation = models.CharField(max_length=128)
class ProfessorProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
slug = models.SlugField(max_length=100,blank=True)
class ExecutiveProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
slug = models.SlugField(max_length=100,blank=True)
I say this to illustrate in my code that I have a slug field in the model User
that takes the value of the username field and I have a field slug
in each user profile for the purposes of the profile url profile/<slug>
with which I display the n forms that I need from the profile
But in our function-based view account_profiles_view()
, what is done is to re-record that profile data, or create a new user instance to which those profiles belong, since when I go and edit them, I am not updating them but the system try to create an additional record with the same id
or pk
and it produces a IntegrityError
and it is obvious, because at no time do we say that we are updating, right?
django.db.utils.IntegrityError: duplicate key value violates unique constraint "accounts_studentprofile_user_id_key"
DETAIL: Key (user_id)=(14) already exists.
It turns out that this (user_id)=(14)
is the id or primary key of that user in my model User
, but in my model StudentProfile
(since it has that profile) it has a id=7
in ProfessorProfile it has a id=8
and in ExecutiveProfile it has aid=7
As what I want is that my function account_profiles_view()
allows me to update the profile data of a user regardless of what their profiles are, so what I think (if I'm not wrong of course) that I should do is update the profile record of that user in some way way by interpreting the user id that comes in the request, something like this? :
User = get_object_or_404(User, pk=pk)
That is to say that my function account_profiles_view()
would have an attribute as a parameter pk
in addition to the request and that this update should be done after verifying that the operation will be a method.POST
The issue is that I am not sure how to do it and I think that finally my function account_profiles_view()
would be like this according to how you have been building it, adding the possibility of updating the user registration for their profiles:
@login_required
# No tengo claro si con estos tres parámetros, se supone que si
# porque el slug lo necesito si o si para visualizar el perfil
def account_profiles_view(request, slug, pk):
user = request.user
_forms = []
if user.is_student:
profile = user.get_student_profile()
_forms.append(forms.StudentProfileForm)
if user.is_professor:
profile = user.get_professor_profile()
_forms.append(forms.ProfessorProfileForm)
if user.is_executive:
profile = user.get_executive_profile()
_forms.append(forms.ExecutiveProfileForm)
if request.method == 'POST':
formularios =[Form(data = request.POST) for Form in _forms]
if all([form.is_valid() for form in formularios]):
for form in formularios:
# No tengo claro como incluir esta parte del update
# y como indicarla en las operaciones posteriores que
# me explicabas
user = get_object_or_404(User, pk=pk)
profile = form.save(commit=False)
profile.user = user
profile.save()
return redirect('dashboard')
else:
formularios = [Form() for Form in _forms]
data = {form.__class__.__name__.__str__().lower(): form for form in formularios}
data['userprofile'] = profile
return render(request, 'accounts/profile_form.html', data,)
At the moment my result when trying to "update" a user's profile is this:
And it's obvious why I don't do an update, but if the problem is in the del line, profile.save()
but if I make the variable profile
take the value of user
the update del pk = pk
, would the thing work?
It is something that I am not sure, I did it, but I wanted to share it with you for your guidance if possible, since it does not work
End of addition to update
I make use of these methods in my views at the time I evaluate each request, what type of user is it, to also call their profile, in this way and seek to somehow send this data to the template, either through some context/ dictionary or using the function render()
as it would be the case if we did everything in a function-based view.
It would be something like this what I would continue doing
if user.is_student:
profile = user.get_student_profile()
_forms.append(forms.StudentProfileForm)
if user.is_professor:
profile = user.get_professor_profile()
_forms.append(forms.ProfessorProfileForm)
if user.is_executive:
profile = user.get_executive_profile()
_forms.append(forms.ExecutiveProfileForm)
return render({'userprofile':profile})
# Aquí ¿enviaría los datos de perfil de un usuario
# en caso de tener los tres perfiles?
In such a case, the view based on function and according to what German raised in his answer, would be like this:
@login_required
def account_profiles_view(request, slug):
user = request.user
_forms = []
if user.is_student:
profile = user.get_student_profile()
_forms.append(forms.StudentProfileForm)
if user.is_professor:
profile = user.get_professor_profile()
_forms.append(forms.ProfessorProfileForm)
if user.is_executive:
profile = user.get_executive_profile()
_forms.append(forms.ExecutiveProfileForm)
if request.method == 'POST':
formularios =[Form(data = request.POST) for Form in _forms]
if all([form.is_valid() for form in formularios]):
for form in formularios:
profile = form.save(commit=False)
profile.user = user
profile.save()
return redirect('dashboard')
else:
formularios = [Form() for Form in _forms]
data = {form.__name__.__str__.lower(): form for form in formularios}
return render(request, 'accounts/profile_form.html', data, {'userprofile':profile})
And in my template, I don't know if I'm properly calling each form instance submitted from the view:
<form method="POST">
{% csrf_token %}
{% if userprofile.user.is_student %}
<div align="center"><i>My Student Profile data</i></div>
{% bootstrap_form studentprofileform %}
{% endif %}
{% if userprofile.user.is_professor %}
<div align="center"><i>My Professor Profile data</i></div>
{% bootstrap_form professorprofileform %}
{% endif %}
{% if userprofile.user.is_executive %}
<div align="center"><i>My Executive Profile data</i></div>
{% bootstrap_form executiveprofileform %}
{% endif %}
<input type="submit" value="Save Changes" class="btn btn-default">
</form>
When I test my profile/username url
url(r"^profile/(?P<slug>[\w\-]+)/$",
views.account_profiles_view,
name='profile'
),
I see this error:
File "/home/bgarcial/workspace/ihost_project/accounts/views.py", line 233, in account_profiles_view
data = {form.__name__.__str__.lower(): form for form in formularios}
File "/home/bgarcial/workspace/ihost_project/accounts/views.py", line 233, in <dictcomp>
data = {form.__name__.__str__.lower(): form for form in formularios}
AttributeError: 'StudentProfileForm' object has no attribute '__name__'
[10/Apr/2017 18:43:40] "GET /accounts/profile/zidane/ HTTP/1.1" 500 86615
Viendo el traceback veo que el inconveniente se referencia a la forma en como se están enviando al template en las instancias de formularios creadas en el diccionario data
No estoy muy familiarizado con la forma en como llevar a cabo el concepto de compresión de listas y diccionarios (que creo que es la forma en como lo estas haciendo ¿cierto?, me corriges si me equivoco por favor) pero entiendo que lo que se hace aquí es:
si la operación de request
de un usuario es POST
, se genera una lista
con todos los forms en donde se haya hecho dicha operación (es decir si esta en uno o dos o los tres perfiles de campos de formularios ).
Acto seguido, si todos los formularios contenidos en esa lista son validos
if all([form.is_valid() for form in formularios]): ...
Itere sobre cada uno y guardelos
Y por ultimo se forma el diccionario data
en donde se envia cada instancia del formulario tomando su nombre respectivo, convirtiendolo a minúsculas y como son tres formularios se itera sobre la lista con todos los formularios generados.
Entiendo, que eso es lo que se hace aquí ¿verdad?
data = {form.__name__.__str__.lower(): form for form in formularios}
Y es bastante coherente y con sentido el planteamiento e implementación. La cosa es que al obtener el mensaje de:
AttributeError: 'StudentProfileForm' object has no attribute '__name__'
No acabo de entender porque para tomar el nombre sería '__name__'
pues creo que fue una convención que utilizaste para ilustrarme acerca de como sería la solución, y que en lugar de __name__
para tomar un nombre como el que se intenta, ¿el sistema esta buscando o debe buscar un atributo que este en los formularios cuando fueron definidos?
Es decir ¿cualquiera de los campos de cada formulario que venga?
Pero no le hayaría sentido poner un nombre de campo, dado que en cad request de url profile
pueden venir o StudentProfileForm
o ProfessorProfileForm
o ExecutiveProfileForm
, pero cada uno tiene nombres propios de campos.
Por esta razón es que veo que en el planteamiento e implementación German intenta darle que el nombre de clave para el formulario que se envie sea el nombre del mismo y en minusculas para llamarlo en el template, lo que tiene sentido, pero entonces el problema de AttributeError: 'StudentProfileForm' object has no attribute '__name__'
que no acabo de comprender.
Pero que veo que con su planteamiento voy un paso mas cerca de obtener la solución, dado que en el request en el traceback estan todos los campos de los formularios que se envian.
La mejor opción como te comentaba en los comentarios, es hacer de tu CBV un FBV donde tendrías mas control, por algunos temas que te comentare mas adelante, de modo que quedaría algo como así:
views.py
Como ves, no me metí con el método
get_user_profile
porque no se lo que hace cada método, pero esto es una forma de verificar todos los formularios, y los recogerias desde el template como{{ studentprofileform }}
tendrías todo más controlado. Esta forma es un poco automatizada, podrías hacerla mas manual y personalizada, que igual trabajarás mejor. Asegurate de mostrar los errores de los formularios para los usuarios desde el template, para que siempre lleguen los formularios adecuadamente llenos.Ahora, en una CBV, el proceso es un tanto diferente, ya que las CBV estan diseñadas para usar un solo formulario, en tu caso usarás tantos formularios como permisos tenga el usuario, por lo que no es muy cómodo. Solo para darte una idea de lo que seria trabajar esto en CBV sería algo así:
views.py
Porque al momento de hacer un POST, las CBV solo verificaran el formulario que obtengan por medio de
get_form_class
y ese formulario será el único que van a proceder a validar. Se puede llegar a validar los 3 formularios a la vez, pero tendrías que sobreescribir muchas partes de código y lo mas sencillo y rápido, será usar un FBV.Cualquier duda, comenta.
ACTUALIZACIÓN
Actualicé la parte donde te arrojaba error:
Lo cambié por:
Que básicamente para responder las otras preguntas, todo en python es un objeto, y la forma de crear objetos es mediante una clase, a la cual desde una instancia, se puede acceder mediante la propiedad 'magica' o 'privada'
__class__
, recuerda que este retorna la clase sin mas, y esta clase, por defecto tiene otra propiedad privada llamada__name__
el cual te retorna el nombre en string de una clase, que será el mismo nombre con el que la llamaste, es decir con el que creas la clase, el que va luego de la palabra reservadaclass
, luego llamo a su método en string, para asegurarme que siempre retorne un string y llamo a lower, la idea de hacer esto es poder ubicarlo en un diccionario de python y que puedas acceder a ello luego de una forma mas facil y legible para el humano. Esto solucionaria elAttributeError
Lo de data, es un simple diccionario de python, el cual será las variables que el django internamente enviará al procesador de contextos y su motor de plantillas para hacer el render de los templates. Consta de llave: valor, en este caso en la compresión de listas, lo que estamos haciendo es algo parecido a esto:
La explicación de esta linea:
Es que usted solo debería guardar datos en la base de datos, si TODOS los formularios que le envió al usuario están correctos, de no ser así, esta linea no tendría sentido y habría que replantearla. Por eso verifico primero que todos los formularios hayan sido llenados.
To conclude, having resolved several doubts, I wanted to help you clarify the part of the profiles, the reason why I did not do anything with the profiles a while ago is that you are overwriting them, each time you enter an if, it overwrites it accordingly to a value, so you will have problems in your code if you leave it like this, let's analyze it, I suggest that I am a user who is a student, an executive, and also a teacher:
If you look at the comments, you'll notice that the variable
profile
depends on the order in which you ask the if questions, and will always get the value of the last if that evaluated to True. But leaving that aside, I don't know what importance you give to the profile, however, the proper way to send said variable to the template is in this way: