django forms logged in user problem - python

I am writing an application to help employees track projects the are working on.
Part of the form should allow logged in employees to click a drop down and see all projects they are working on. This is the part I am struggling with; specifically getting ONLY the logged in user's projects populated in a drop down. Any help or insight is much appreciated. Thanks……
models.py
class Photo(models.Model):
image = models.ImageField(upload_to='uploads/images/photo')
title = models.CharField(max_length=50)
def __unicode__(self):
return self.title
class Employee(models.Model):
user = models.ForeignKey(User, unique=True)
photo = models.ImageField(upload_to='uploads/images')
department = models.ForeignKey(Department, null=True)
phone = PhoneNumberField("Phone")
def __unicode__(self):
return self.user.get_full_name()
class Projects(models.Model):
name = models.CharField(max_length=40)
student = models.ForeignKey(Student)
photos = models.ManyToManyField(Photo, blank=True, null=True)
forms.py
class ProjectsForm(forms.ModelForm):
employee = get_object_or_404(Employee, user=user)
employee_projects = employee.projects_set.all()
name = forms.ModelChoiceField(queryset=employee_projects,
empty_label="(Select a Project)", required=True)
class Meta:
model = Projects

You need to put first two lines from ProjectsForm class definition to its initialization method and change them a bit.
class ProjectsForm(forms.ModelForm):
name = forms.ModelChoiceField(queryset=Employee.objects.all(),
empty_label="(Select a Project)", required=True)
class Meta:
model = Projects
def __init__(self, user, *args, **kwargs):
super(self, ProjectsForm).init(*args, **kwargs)
employee = get_object_or_404(Employee, user=user)
self.fields['name'].queryset = employee.projects_set.all()
Now, some explanation. Hope someone will find it useful.
In your original ProjectsForm definition, you're trying to get employee's projects when your class is defined. But this happens once your forms.py file is compiled, which takes place rarely (when you change code, for example). Also, you of course have no data that is necessary to filter projects, i.e., no user object, at that stage.
Instead, you need to do this each time the class is initialized. Initialization in Python takes place in special __init__() method. For example, when you're doing something like
form = ProjectsForm(data=request.POST)
in your view, what happens is that ProjectsForm.__init__(data=request.POST) is called to initialize ProjectsForm into an instance.
So, in our new definition, we're requiring a new argument (user) to be passed to the form when it's instantiated. So you can do something like this in your view:
form = ProjectsForm(request.user, data=request.POST)
In new initialization method, first we're calling the initialization method of parent class (which does some internal django things and should be called anyway), then use argument user to get related employee, and then assign a queryset with that employee's projects to the name field.
Sorry if I'm being too verbose.
See also:
Django, passing information when modifying queryset for ModelForm
django - dynamic select fields in forms

Why not have a many to many field in the Employee model that points to all the projects the employee can work on?
https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.ManyToManyField
I think that would be the best way to do it.

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)

Displaying fields other than pk in a Django Form with a ModelChoiceField

I am building a website where users can upload files and can attach uploads to projects that they created beforehand. The upload is done with a django form where the user can specify the title, comments, etc... There is also a dropdown list where the user can choose from the existing projects that he created (list of projects is dependent on user)
As of now the dropdown only shows the (autogenerated) project id which is the pk of the model Project.
I want the dropdown to show the names of the projects and not the project ID which is not very meaningful for the user.
I have already tried
to_field_name='name'
but that didn't work
I have also tried
Project.objects.filter(user=user).values_list('name')
or
Project.objects.filter(user=user).values('name')
the last two options show the project name in {'projectname} but when I select them and submit the form the error "Select a valid choice. That choice is not one of the available choices."
This is my code:
models.py
class Upload(models.Model):
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
upload_date = models.DateTimeField(default=timezone.now)
comments = models.CharField(max_length=10000, null=True)
title = models.CharField(max_length=10000, null=True)
project = models.CharField(max_length=99, default='--None--')
forms.py
class UploadForm(ModelForm):
project = ModelChoiceField(label='Select Project', queryset=Project.objects.all(), to_field_name='name',
empty_label='--Select Project--')
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super(UploadForm, self).__init__(*args, **kwargs)
if user is not None:
self.fields['project'].queryset = Project.objects.filter(user=user)
class Meta:
model = Upload
fields = ['title', 'project', 'upload_date', 'comments']
According to docs
The str() method of the model will be called to generate string representations of the objects for use in the field’s choices. To provide customized representations, subclass ModelChoiceField and override label_from_instance. This method will receive a model object and should return a string suitable for representing it.
https://docs.djangoproject.com/en/2.2/ref/forms/fields/#modelchoicefield
so you should define __str__() method for Project model e.g.
def __str__(self):
return self.name

How to change field in ModelForm generated html form?

I'm making one of my first django apps with sqlite database. I have some models like for example:
class Connection(models.Model):
routeID = models.ForeignKey(Route, on_delete=models.CASCADE)
activityStatus = models.BooleanField()
car = models.ForeignKey(Car, on_delete=models.CASCADE)
class Route(models.Model):
name = models.CharField(max_length=20)
and forms
class RouteForm(ModelForm):
class Meta:
model = Route
fields = ['name']
class ConnectionForm(ModelForm):
class Meta:
model = Connection
fields = ['routeID', 'activityStatus', 'car']
And in my website, in the url for adding new Connection, I have cascade list containing RouteIDs. And I'd like it to contain RouteName, not ID, so it would be easier to choose. How should I change my ConnectionForm, so I could still use foreign key to Route table, but see RouteName instead of RouteID?
For now it's looking like this, but I'd love to have list of RouteNames, while still adding to Connection table good foreign key, RouteID
Update the Route Model's __str__ method:
class Route(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return self.name
Because the __str__() method is called whenever you call str() on an object. Django uses str(obj) in a number of places like in Modelform. By default it returns id or pk that is why you were seeing ids in model form. So by overriding it with name, you will see the names appear in choice field. Please see the documentation for more details on this.

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 multiple user profiles - Display user fields inline in admin

I am relatively new to Django and I'm trying to achieve something that is not quite clear in the documentation. My application requires multiple types of users. So, I have extended django User, creating a Profile model that contains additional fields common in all User types:
USER_TYPES = (
('A', 'UserTypeA'),
('B', 'UserTypeB'),
('C', 'UserTypeC'),
)
class Profile(models.Model):
user = models.OneToOneField(User, unique=True)
about = models.TextField(blank=True)
user_type = models.CharField(max_length=3, choices=USER_TYPES, default='A')
def save(self, *args, **kwargs):
try:
existing = Profile.objects.get(user=self.user)
self.id = existing.id #force update instead of insert
except Profile.DoesNotExist:
print "Profile not created yet"
models.Model.save(self, *args, **kwargs)
def create_user(sender, instance, created, **kwargs):
print "User Profile Creation: False"
if created:
print "User Profile Creation: ", created
Profile.objects.create(user=instance)
post_save.connect(create_user, sender=Profile)
In settings.py I have set:
AUTH_PROFILE_MODULE = 'users.Profile'
After that i have defined my UserTypeX models deriving from Profile models like this:
class UserTypeA(Profile):
phone_number = models.CharField(max_length=13,blank=False)
class UserTypeB(Profile):
company_name = models.CharField(max_length=30,blank=False)
phone_number = models.CharField(max_length=13,blank=False)
def __init__(self, *args, **kwargs):
kwargs['user_type'] = 'B'
super(UserTypeB, self).__init__(*args, **kwargs)
...
I've registered those user models to admin so that I could manage my users independently.
The default admin behavior displays correctly all the Profile and UserTypeX fields and there is a select box (with a plus button next to it) for the User field - due to the OneToOneField relationship between Profile and User models. So whenever I want to create a new UserTypeX, I have to press the plus button and fill in a new popup window all the default User django defined fields.
What I am struggling to do now is display those User fields, not in a new popup window but inline in my UserTypeX add/edit page. I've read the documentation about StackedInlines and TabularInlines but those doesn't fit my case because I want to inline parent fields in an ancestor's add/edit page and not vice versa.
Is there any suggested solution with example code (please!) for that problem? Thank's in advance!
So, to make things short, is there a way to display User fields (instead of the select/add functionality due to OneToOneField relationship) in Profile add/edit screen in admin?
Update: A related question (unanswered though...) that briefly addresses the problem is:
Reverse Inlines in Django Admin
No, you cannot. But you can make B inline in A if you want. Or maybe you can manipulate how django display it in
def unicode(self):
models.py
class A(models.Model):
name = models.CharField(max_length=50)
def __unicode__(self):
return self.name
class B(models.Model):
name = models.CharField(max_length=50)
a = models.ForeignKey(A)
admin.py
class B_Inline(admin.TabularInline):
model = B
class A_Admin(admin.ModelAdmin):
inlines = [
B_Inline,
]
admin.site.register(A, A_Admin)
admin.site.register(B)
Or maybe you want to use many-to-many relationship?
models.py
class C(models.Model):
name = models.CharField(max_length=50)
def __unicode__(self):
return self.name
class D(models.Model):
name = models.CharField(max_length=50)
cs = models.ManyToManyField(C)
admin.py
class C_Inline(admin.TabularInline):
model = D.cs.through
class D_Admin(admin.ModelAdmin):
exclude = ("cs",)
inlines = [
C_Inline,
]
admin.site.register(C)
admin.site.register(D, D_Admin)
I don't see how this is going to work - you have told django to use users.Profile as your profile model but you actually want to use it's children. Plus as you say you're wanting to use the inline forms the "wrong way around". Without knowing more about what exactly you're trying to achieve I'd suggest that you could fix the inline form issue by defining a custom view to edit the user details rather than using django admin. As for the different profile types - I'd suggest just defining a profile type field in the model and then hiding/showing different fields/properties depending on its value

Categories