I'm creating a django app for an education company, and I created a custom user model and two classes (Student and Teacher) that will inherit from the User model through a one-to-one relationship.
I'm trying to avoid the situation where a teacher and a student both use the same user object. Thus, I have a user_type char field in my User object, and the idea is when a user is set to a teacher, the field will be updated. Then if I try to make the user a student, it should throw an error.
I'm trying to do the check in the clean function - after the clean() function is called in the save() function, the user_type seems to be updated, but when I actually test, it seems user_type is returning a blank string. I'm wondering if someone could point me the right direction.
class User(AbstractUser):
user_type = models.CharField(max_length=20, choices=USER_TYPES, blank=True)
class Teacher(TimeStampedModel):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
def clean(self):
if self.user.user_type and self.user.user_type !='teacher':
raise ValidationError('Some error')
else:
self.user.user_type = 'teacher'
def save(self, *args, **kwargs):
self.clean()
#this prints 'teacher'
print(self.user.user_type)
super().save(*args, **kwargs)
tests.py
#this test fails
def test_should_set_user_type_automatically_to_teacher(self):
user = User.objects.create(username='tangbj', first_name='Dan')
Teacher.objects.create(user=user)
teacher = Teacher.objects.get(user__username='tangbj')
print(teacher.user.user_type)
self.assertEqual(teacher.user.user_type, 'teacher')
You are saving the Teacher instance, not the User instance. When you assign the user_type, you should do a save(..) to the User instance. Records aren't magically updated.
Said in another way; you are modifying an instance attribute, but you are not saving the instance.
Related
I am working on a Hostel management system and new in Django field. I have a model Student and Bed . The models code is below:
class Bed(models.Model):
name = models.CharField("Bed No.",max_length=200)
room = models.ForeignKey(Room, on_delete=models.CASCADE, default='1')
class Meta:
verbose_name_plural = "Bed"
def __str__(self):
return self.name
class Student(models.Model):
name = models.CharField("name",max_length=200)
cell_no = models.CharField("cell No",max_length=200)
photo = models.ImageField(upload_to ='students_pics/')
emergency_cell_no = models.CharField("Emergency Cell No", max_length=200)
bed = models.ForeignKey(Bed, on_delete=models.CASCADE, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name_plural = "Student"
def __str__(self):
return self.name
I want that when I select a bed from Student Model dropdown, there should only display the beds which are not already assigned to some other students.
I have tried something like:
bed = models.ForeignKey(Bed, on_delete=models.CASCADE, null=True, blank=True).exclude(----)
but it does not work. I have searched around Please help.
you must filter this field from admin file.
solution 1 for filtering in admin)
for example, define a status field for the bed model with true value as default, after that override save method into Student model and with creating a student with a select bed, set status bed field to false
2 for filtering)
query on all students and check which beds don't assigned, then show into the admin
but for admin file you have to follow something like the below link
enter link description here.
or
link 2
or search django admin filtering key word
The answer above works as expected in the question. But when I update the Student Form, the form field of "bed" is not pre populated with the values. I have also used instance=student. My code for update view is below:
def updatestudent(request,id):
student = Student.objects.get(pk=id)
form = StudentForm(instance=student)
if request.method == 'POST':
#print('Printing Post:', request)
form = StudentForm(request.POST, instance=student)
if form.is_valid():
form.save()
messages.success(request, "Student updated successfully.")
return redirect('/student/view/'+id)
context= {'form':form}
return render(request, 'new-student.html', context)
Assuming you're using a form and do not want to do this in the admin, you can do the following. First, add a related_name= argument to your bed field on the Student model, like so (could be any string) and make migrations and migrate:
bed = models.ForeignKey(Bed, on_delete=models.CASCADE, null=True, blank=True, related_name='all_beds')
Then, using a model form with a custom __init__ method, you can populate your dropdown with only those beds that are not assigned.
class StudentForm(forms.ModelForm):
class Meta:
model = Student
fields = ['name', 'cell_no', 'emergency_cell_no', 'bed', 'photo']
def __init__(self, *args, **kwargs):
super(StudentForm, self).__init__(*args, **kwargs)
self.fields['bed'].queryset = Bed.objects.filter(all_beds__bed__isnull=True)
Here's what's happening here. The init method is populating the choices on the bed field with whatever .queryset you pass to it.
In this case, you're using the related_name "all_beds" to refer back to the Student model. The all_beds__bed is looking at Student.bed, and all_beds__bed__isnull=True is asking the database to return all Bed instances that do not have some existing relation to Student.bed. In other words, if a Bed instance has some populated bed field on the Student model, do not add it to the queryset.
When you call this model form in your view, for example:
form = StudentForm()
...it will populate accordingly. If you need that form to be dynamically created with data only available in your view, say an ID number or user data, you can instantiate the modelform class in your view. It doesn't HAVE to be in a seperate forms.py file.
Using Dynamic Forms
Now let's suppose you want to have an update view that excludes all the assigned beds, BUT includes the bed that was assigned to the student being updated.
Here's an example of how that could work, using a Q Object query in a dynamically instantiated form class within the view. You can do this by writing and calling a function that creates the ModelForm. The Q Object query combines unassigned beds with the one bed that IS assigned to the student instance.
Note that the create_form function takes in the request object. This isn't necessary as written, but I'm showing it because you may have a case where your logged in user wants to see the bed they have selected, in which case you could use Q(all_beds__user=request.user), assuming you have added a user foreign key to Student your model. Of course another way of doing this is to query the student based on request.user.
from django.db.models import Q
def updatestudent(request,id):
student = Student.objects.get(pk=id)
form = StudentForm(instance=student)
def create_form(request, student):
class StudentForm(forms.ModelForm):
class Meta:
model = Student
fields = ['name', 'cell_no', 'emergency_cell_no', 'bed', 'photo']
def __init__(self, *args, **kwargs):
super (StudentForm, self).__init__(*args, **kwargs)
self.fields['bed'].queryset = Bed.objects.filter(Q(all_beds__bed__isnull=True)|Q(all_beds__id=student.id))
return StudentForm
StudentForm = create_form(request, student)
if request.method == 'POST':
form = StudentForm(request.POST, instance=student)
if form.is_valid():
form.save()
messages.success(request, "Student updated successfully.")
return redirect('/student/view/'+id)
else:
form = StudentForm(instance=student)
context= {'form':form}
return render(request, 'new-student.html', context)
For more information about complex queries using Q objects, check out the documentation here.
I have the following abstract class:
class UserStamp(models.Model):
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True,
related_name='%(app_label)s_%(class)s_created_by', on_delete=models.CASCADE)
updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
related_name='%(app_label)s_%(class)s_updated_by', on_delete=models.CASCADE)
class Meta:
abstract = True
I have a custom User that inherits from User.
class User(AbstractBaseUser,PermissionsMixin, UserStamp):
account = models.ForeignKey(Account, blank=True, null=True, related_name='owner',on_delete=models.CASCADE)
The User can create/update himself or by other user.
When the user create/update himself I don't have anything for created_by, update_by.
The user can be created using Django Admin or outside Django Admin;
In Django Admin the user can be created by staff, outside Django is self created;
Also there is superuser that is created in terminal;
Regarding the update the user in both Django Admin and outside can be self updated or by another user.
I thought on using post_save or a custom signal. The issue is that request.user is not available in the model, but in View, and controlling the View in Admin and also in terminal(superuser) is a bottleneck.
Maybe trying to do a query after save passing the instance, but I don't exactly know how to combine all of them signal/query, check superuser.
Create your own custom signals.
signals.py
from django.dispatch import Signal
# you can pass any number of arguments as per your requirement.
manage_user = Signal(providing_args=["user", "is_updated"])
def user_handler(sender, **kwargs):
# `user` who created/updated object i.e `request.user`
user = kwargs['user']
# `is_updated` will be `False` if user object created.
is_updated = kwargs['is_updated']
# `sender` is the user object which is created/updated.
...
# do your stuff
manage_user.connect(user_handler)
models.py
Override save() method of your custom user class.
from .signals import manage_user
class User(...):
...
# call this save method like obj.save(created_by=request.user)
def save(self, created_by, *args, **kwargs):
super().save(*args, **kwargs)
# is_updated will be True if user object is updated
manage_user.send(sender=self, user=created_by, is_updated=True if self.id else False)
send manage_user signal when user model changed, just like post_save signal,
but now you have control over parameters.
UPDATE
If you are using django admin to create user you can overide save_model, you have request object there.
from django.contrib import admin
#admin.register(User)
class UserAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
super(UserAdmin, self).save_model(request, obj, form, change)
manage_user.send(sender=self, user=request.user, is_updated=True if self.id else False)
I'd like to do something like this:
class Task(models.Model):
...
created_by = models.ForeignKey(User, **default=[LoggedInUser]** blank=True, null=True, related_name='created_by')
Is this possible? I couldn't find what's the proper way to get the logged in user, apart from doing request.user, in a view, which doesn't seem to work here.
PS_ I realise I could initialize the Model data by other means, but I think this is the cleanest way.
If you want to achieve this within the admin interface, you can use the save_model method. See below an example:
class List(models.Model):
title = models.CharField(max_length=64)
author = models.ForeignKey(User)
class ListAdmin(admin.ModelAdmin):
fields = ('title',)
def save_model(self, request, obj, form, change):
obj.author = request.user
obj.save()
No, you can't do it this way. Django (and Python) has pretty much zero global values, and that's a Good Thing(tm). Normally you get the current user in the view(request) with request.user. You can then pass that as a param to various methods/functions, but trying to set a global user will only lead to tears in a multi-threaded environment.
There should be a bumper sticker that says, Globals are Evil. This will give you a good idea about my Number One problem with PHP.
SOLVED:
I will use an example, but the important part is the funciton on the views.py.
User is automatically available by django.
Note the 'autor' model field has a ForeignKey to the 'User'.
In the 'def form_valid' below I assign the currently logged in user as the default value.
If this is your model:
class ProspectoAccion(models.Model):
"""
Model representing a comment against a blog post.
"""
descripcion = models.TextField(max_length=1000)
autor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
accion_date = models.DateTimeField(auto_now_add=True)
prospecto= models.ForeignKey(Prospecto, on_delete=models.CASCADE)
tipo_accion = models.ForeignKey('Accion', on_delete=models.SET_NULL, null=True)
And you have a class based view, do the following:
class ProspectoAccionCreate(LoginRequiredMixin, CreateView):
"""
Form for adding una acción. Requires login (despues poner)
"""
model = ProspectoAccion
fields = ['tipo_accion','descripcion',]
def form_valid(self, form):
#Add logged-in user as autor of comment THIS IS THE KEY TO THE SOLUTION
form.instance.autor = self.request.user
# Call super-class form validation behaviour
return super(ProspectoAccionCreate, self).form_valid(form)
HERE IS AN EXAMPLE FROM THE DOCUMENTATION:
https://docs.djangoproject.com/en/2.0/topics/class-based-views/generic-editing/#models-and-request-user
If use ModelForm, the following will fill a default value for a special field. such as, owner filed is a charfield for user's name
def fillview(request):
instance = YourModel(owner=request.user.username)
form = YourModelForm(instance=instance)
if request.method == 'POST':
form = YourModelForm(request.POST, request.FILES)
if form.is_valid():
pass
return render(request, 'success.html')
return render(request, 'fill.html', {'form': form})
When logged in, you could see owner filed is current user's name.
By default, Django already creates a "created_by" attribute. You don't need to create your own.
If you nonetheless need to save this information separately to, let's say, have the possibility to change the user later on without affecting the original creator value, then you could override the save function to retrieve the value that Django assigns by default to "created_user":
class Application(models.Model):
property = models.ForeignKey(Property, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='applications', editable=False, null=True)
...
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if not self.user:
self.user = self.created_by
super(Application, self).save(*args, **kwargs)
My models:
class UserProfile(models.Model):
TYPES_CHOICES = (
(0, _(u'teacher')),
(1, _(u'student')),
)
user = models.ForeignKey(User, unique=True)
type = models.SmallIntegerField(default=0, choices=TYPES_CHOICES, db_index=True)
cities = models.ManyToManyField(City)
class City(models.Model):
name = models.CharField(max_length=50)
slug = models.SlugField(max_length=50)
In admin.py:
admin.site.unregister(User)
class UserProfileInline(admin.StackedInline):
model = UserProfile
class UserProfileAdmin(UserAdmin):
inlines = [UserProfileInline]
admin.site.register(User, UserProfileAdmin)
#receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
"""Create a matching profile whenever a user object is created."""
if created:
profile, new = UserProfile.objects.get_or_create(user=instance)
But when I add new user and select a city I get that error: IntegrityError at /admin/auth/user/add/
(1062, "Duplicate entry '3' for key 'user_id'")
What is wrong with my code? If I don't select any city - user is added properly. Some way, user is being added to UserProfile more than once.
I had this same issue recently. It actually makes perfect sense when you think about it. When you save a form with inlines in the admin, it saves the main model first, and then proceeds to save each inline. When it saves the model, your post_save signal is fired off and a UserProfile is created to match, but now it's time to save the inlines. The UserProfile inline is considered new, because it didn't exist previously (has no pk value), so it tries to save as an entirely new and different UserProfile and you get that integrity error for violating the unique constraint. The solution is simple. Just override UserProfile.save:
def save(self, *args, **kwargs):
if not self.pk:
try:
p = UserProfile.objects.get(user=self.user)
self.pk = p.pk
except UserProfile.DoesNotExist:
pass
super(UserProfile, self).save(*args, **kwargs)
Essentially, this just checks if there's an existing UserProfile for the user in question. If so, it sets this UserProfile's pk to that one's so that Django does an update instead of a create.
I'd like to do something like this:
class Task(models.Model):
...
created_by = models.ForeignKey(User, **default=[LoggedInUser]** blank=True, null=True, related_name='created_by')
Is this possible? I couldn't find what's the proper way to get the logged in user, apart from doing request.user, in a view, which doesn't seem to work here.
PS_ I realise I could initialize the Model data by other means, but I think this is the cleanest way.
If you want to achieve this within the admin interface, you can use the save_model method. See below an example:
class List(models.Model):
title = models.CharField(max_length=64)
author = models.ForeignKey(User)
class ListAdmin(admin.ModelAdmin):
fields = ('title',)
def save_model(self, request, obj, form, change):
obj.author = request.user
obj.save()
No, you can't do it this way. Django (and Python) has pretty much zero global values, and that's a Good Thing(tm). Normally you get the current user in the view(request) with request.user. You can then pass that as a param to various methods/functions, but trying to set a global user will only lead to tears in a multi-threaded environment.
There should be a bumper sticker that says, Globals are Evil. This will give you a good idea about my Number One problem with PHP.
SOLVED:
I will use an example, but the important part is the funciton on the views.py.
User is automatically available by django.
Note the 'autor' model field has a ForeignKey to the 'User'.
In the 'def form_valid' below I assign the currently logged in user as the default value.
If this is your model:
class ProspectoAccion(models.Model):
"""
Model representing a comment against a blog post.
"""
descripcion = models.TextField(max_length=1000)
autor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
accion_date = models.DateTimeField(auto_now_add=True)
prospecto= models.ForeignKey(Prospecto, on_delete=models.CASCADE)
tipo_accion = models.ForeignKey('Accion', on_delete=models.SET_NULL, null=True)
And you have a class based view, do the following:
class ProspectoAccionCreate(LoginRequiredMixin, CreateView):
"""
Form for adding una acción. Requires login (despues poner)
"""
model = ProspectoAccion
fields = ['tipo_accion','descripcion',]
def form_valid(self, form):
#Add logged-in user as autor of comment THIS IS THE KEY TO THE SOLUTION
form.instance.autor = self.request.user
# Call super-class form validation behaviour
return super(ProspectoAccionCreate, self).form_valid(form)
HERE IS AN EXAMPLE FROM THE DOCUMENTATION:
https://docs.djangoproject.com/en/2.0/topics/class-based-views/generic-editing/#models-and-request-user
If use ModelForm, the following will fill a default value for a special field. such as, owner filed is a charfield for user's name
def fillview(request):
instance = YourModel(owner=request.user.username)
form = YourModelForm(instance=instance)
if request.method == 'POST':
form = YourModelForm(request.POST, request.FILES)
if form.is_valid():
pass
return render(request, 'success.html')
return render(request, 'fill.html', {'form': form})
When logged in, you could see owner filed is current user's name.
By default, Django already creates a "created_by" attribute. You don't need to create your own.
If you nonetheless need to save this information separately to, let's say, have the possibility to change the user later on without affecting the original creator value, then you could override the save function to retrieve the value that Django assigns by default to "created_user":
class Application(models.Model):
property = models.ForeignKey(Property, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='applications', editable=False, null=True)
...
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if not self.user:
self.user = self.created_by
super(Application, self).save(*args, **kwargs)