M2M using through and form with multiple checkboxes - django

I'd like to create a form allowing me to assign services to supplier from these models. There is no M2M relationship defined since I use a DB used by others program, so it seems not possible to change it. I might be wrong with that too.
class Service(models.Model):
name = models.CharField(max_length=30L, blank=True)
class ServiceUser(models.Model):
service = models.ForeignKey(Service, null=False, blank=False)
contact = models.ForeignKey(Contact, null=False, blank=False)
class SupplierPrice(models.Model):
service_user = models.ForeignKey('ServiceUser')
price_type = models.IntegerField(choices=PRICE_TYPES)
price = models.DecimalField(max_digits=10, decimal_places=4)
I've created this form:
class SupplierServiceForm(ModelForm):
class Meta:
services = ModelMultipleChoiceField(queryset=Service.objects.all())
model = ServiceUser
widgets = {
'service': CheckboxSelectMultiple(),
'contact': HiddenInput(),
}
Here is the view I started to work on without any success:
class SupplierServiceUpdateView(FormActionMixin, TemplateView):
def get_context_data(self, **kwargs):
supplier = Contact.objects.get(pk=self.kwargs.get('pk'))
service_user = ServiceUser.objects.filter(contact=supplier)
form = SupplierServiceForm(instance=service_user)
return {'form': form}
I have the feeling that something is wrong in the way I'm trying to do it. I have a correct form displayed but it is not instantiated with the contact and checkboxes aren't checked even if a supplier has already some entries in service_user.

You are defining services inside your Meta class. Put it outside, right after the beginning of SupplierServiceForm. At the very least it should show up then.
Edit:
I misunderstood your objective. It seems you want to show a multiple select for a field that can only have 1 value. Your service field will not be able to store the multiple services.
So, by definition, your ServiceUser can have only one Service.
If you don't want to modify the database because of other apps using it, you can create another field with a many to many relationship to Service. That could cause conflicts with other parts of your apps using the old field, but without modifying the relationship i don't see another way.

The solution to my problem was indeed to redefine my models in oder to integrate the m2m relationship that was missing, using the through argument. Then I had to adapt a form with a special init method to have all selected services displayed in checkboxes, and a special save() method to save the form using m2m relationship.
class Supplier(Contact):
services = models.ManyToManyField('Service', through='SupplierPrice')
class Service(models.Model):
name = models.CharField(max_length=30L, blank=True)
class ServiceUser(models.Model):
service = models.ForeignKey(Service, null=False, blank=False)
supplier = models.ForeignKey(Supplier, null=False, blank=False)
price = models.Decimal(max_digits=10, decimal_places=2, default=0)
And the form, adapted from the very famous post about toppings and pizza stuff.
class SupplierServiceForm(ModelForm):
class Meta:
model = Supplier
fields = ('services',)
widgets = {
'services': CheckboxSelectMultiple(),
'contact_ptr_id': HiddenInput(),
}
services = ModelMultipleChoiceField(queryset=Service.objects.all(), required=False)
def __init__(self, *args, **kwargs):
# Here kwargs should contain an instance of Supplier
if 'instance' in kwargs:
# We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist.
initial = kwargs.setdefault('initial', {})
# The widget for a ModelMultipleChoiceField expects
# a list of primary key for the selected data (checked boxes).
initial['services'] = [s.pk for s in kwargs['instance'].services.all()]
ModelForm.__init__(self, *args, **kwargs)
def save(self, commit=True):
supplier = ModelForm.save(self, False)
# Prepare a 'save_m2m' method for the form,
def save_m2m():
new_services = self.cleaned_data['services']
old_services = supplier.services.all()
for service in old_services:
if service not in new_services:
service.delete()
for service in new_services:
if service not in old_services:
SupplierPrice.objects.create(supplier=supplier, service=service)
self.save_m2m = save_m2m
# Do we need to save all changes now?
if commit:
self.save_m2m()
return supplier
This changed my first models and will make a mess in my old DB but at least it works.

Related

Is there a way in Django Rest Framework to deliver only specific attributes?

When building a large web backend we are faced with the problem that we have models with a lot of attributes. As we want to reduce the size of the responses for efficiency we use different serializers for our models. Take this simple model for instance:
class Address(models.Model):
country_id = models.ForeignKey(Country, on_delete=models.PROTECT)
postal_code = models.CharField(max_length=10)
street = models.CharField(max_length=176)
number = models.CharField(max_length=20)
additional = models.CharField(max_length=256, blank=True, default='')
city = models.CharField(max_length=176)
state = models.CharField(max_length=50)
user = models.ForeignKey('user.User', on_delete=models.CASCADE)
For this alone we have an ID Serializer, a StringSerializer (for delivering a usable string), a MinimalSerializer (for a list of multiple addresses), a DetailSerializer (with depth=1 for getting country and user details ...
Now with this complexity we face several issues and thought about restructuring the api to include the required fields in the request. Is this possible with django + drf? We believed to find something under Content Negotiation but this is a different topic.
You can dynamically modify fields like this:
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
For more information visit drf docs.

django manytomany model relationship crashing admin on object create

I have an Event object in my postgres db, and created a new Collection object to group events by theme via a ManyToMany field relationship:
class Collection(models.Model):
event = models.ManyToManyField('Event', related_name='collections')
name = models.CharField(blank=True, max_length=280)
slug = AutoSlugField(populate_from='name')
image = models.ImageField(upload_to='collection_images/', blank=True)
description = models.TextField(blank=True, max_length=1000)
theme = models.ManyToManyField('common.Tag', related_name='themes')
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=False)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('collection', args=[self.slug])
def clean(self):
# because of the way db saves M2M relations, collection doesn't have a
# type at this time yet, so image inheritance is
# called from the signal which is triggered when M2M is created
# (that means if an image is later deleted, it won't inherit a new
# one when collection is saved)
if self.image:
validate_hero_image(self.image, 'image')
def save(self, *args, **kwargs):
try:
self.full_clean()
except ValidationError as e:
log.error('Collection validation error (name = %s): %s' % (self.name, e))
return super(Collection, self).save(*args, **kwargs)
in my admin, I'm defining and registering CollectionAdmin like this:
class CollectionAdmin(admin.ModelAdmin):
model = Collection
verbose_name = 'Collection'
list_display = ( 'name', )
however, if I go into admin and attempt to create a Collection "GET /admin/app/collection/add/" 200, the request frequently times out and the query load on my database from the Event M2M relationship seems quite heavy from logging. For reference currently the db has ~100,000 events. are there better ways to (re)structure my admin fields so I can select specific events (by name or id) to add to a Collection without effectively requesting a QuerySet of all events when that view is loaded (or creating them in db via shell)? thanks
There are multiple ways to accomplish this. For example, you could override the form fields the admin uses and specify another widget to use, like NumberInput.
You could also add your event model field to the raw_id_fields attribute of ModelAdmin. By doing so, Django won't try to create a fully populated select input but will offer you a way to search for events manually if needed:
class CollectionAdmin(admin.ModelAdmin):
model = Collection
verbose_name = 'Collection'
list_display = ('name', )
raw_id_fields = ('event', )

Validating a Django field dependently on context

I write the code to edit the list of users which belong to a team. For this I create a form, as below:
class Organization(models.Model):
name = models.CharField(blank=False, verbose_name=_("Name"), help_text=_('Organization Name'), max_length=256)
class Team(models.Model):
organization = models.ForeignKey('Organization')
name = models.CharField(blank=False, verbose_name=_("Name"), help_text=_('Team Name'), max_length=256)
users = models.ManyToManyField('User', related_name='teams')
def __str__(self):
return self.name
class TeamUsersForm(forms.ModelForm):
class Meta:
model = Team
fields = ['users']
users = forms.ModelMultipleChoiceField(queryset=User.objects.filter(request.user.organization), required=False)
def clean_users(self):
users = self.cleaned_data['users']
if users.exclude(organization=request.user.organization):
raise ValidationError(_("Cannot add user from another organization"))
return users
The code above should look into request value to determine the current organization and restrict display and model store only to users from the same organization.
But the above code cannot work, because the value of request is not known at class loading time.
What do you suggest to do?
I thought of two variants:
create a local (to a function) class like the above class TeamUsersForm
dismiss using Django forms for this altogether and use more low-level API
Overide the __init__ of the TeamUsersForm and access request there.
class TeamUsersForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request')
super().__init__(*args, **kwargs)
self.fields['users'] = forms.ModelMultipleChoiceField(queryset=User.objects.filter(self.request.user.organization), required=False)
This implies that when you instantiate your form, you should it this way:
# somewhere in your views.py, probably
f = TeamUsersForm(request.POST, request=request)

Django Models: Reference the same field twice in a fk model

So here's the issue i'm having with this Django Model.
I have a foriegn key to class SecretSantaGroup in class assignees called group.
I want to reference this fk in creator and assignee.
pretty much the data I want is like this:
creator = self.group.members
assignee = self.group.members
But I'm having issues on going about it and could use some help.
I want to be able to reference all the users in that specific group, just having trouble going about it.
class SecretSantaGroups(models.Model):
groupName = models.TextField()
members = models.ManyToManyField(User)
def __str__(self):
return self.groupName
class Meta:
verbose_name_plural = 'Secret Santa Groups'
class assignees(models.Model):
group = models.ForeignKey(SecretSantaGroups)
#person that gives gifts
creator = models.ForeignKey(self.group.members, null=True)
#person who receives gift
assignee = models.ForeignKey(self.group.members, null=True)
EDIT
---I used terrible wording, the assignees class is supposed to be who gets who in the group. 1 person gets another in each secret santa group. so gifter and giftee
class assignees(models.Model):
group = models.ForeignKey(SecretSantaGroups)
#person that gives gifts
giver = models.???(self.group.members, null=True)
#person who receives gift
giftee = models.???(self.group.members, null=True)
Unless I'm mistaken, it seems like what you're trying to do is define an association with all possible users for the SecretSantaGroup and then define which of those users is "assigned" or whatever you want to call it.
I also don't know if you want to edit these within Django admin, or part of a different view, but how I would define the model is as such:
class SecretSantaGroup(models.Model):
name = models.CharField(max_length=255)
creator = models.ForeignKey(User, related_name='creator')
members = models.ManyToMany(User, related_name='members')
assigned = models.ManyToMany(User, related_name='assigned', blank=True, null=True)
def __unicode__(self):
return self.name
If you want to limit the choices of "assigned" in Django admin, you'll need to do this in two steps. First, you would need to assign which members, then you'd need to assign the QuerySet of "assigned" to the objects in members so the choices are limited, and then you can assign which ones you want.
This can be done via a custom form. I have NOT tested this code:
class SecretSantaGroupForm(forms.ModelForm):
class Meta:
model = SecretSantaGroup
def __init__(self, *args, **kwargs):
super(SecretSantaGroupForm, self).__init__(*args, **kwargs)
self.fields['assigned'].queryset = self.instance.members.all()
Then you can assign the custom admin form on your model admin.
If you're doing this on the public side, you'll still need the same type of override, and you'll still have to do this in two steps, as best as I can tell. Hope that helps you out.

Django: why the manytomany choice box only has on side

I have extended the group model, where I added some manytomany fields, and in the admin page, it likes this:
However, what I expected is this:
Here is how I implemented the m2m field:
class MyGroup(ProfileGroup):
mobile = models.CharField(max_length = 15)
email = models.CharField(max_length = 15)
c_annotates = models.ManyToManyField(Annotation, verbose_name=_('annotation'), blank=True, null=True)
c_locations = models.ManyToManyField(Location, verbose_name=_('locations'), blank=True, null=True)
And in the database there is a relational form which contains the pairs of group_id and location_id.
Is there anyone who knows how to do it? Thanks!
EDIT:
I implemented as above, the multiple select box actually shows up, but it cannot save... (Sorry, I was working on a virtual machine and it's offline now, so I have to clip the code from screen)
latest 2017
govt_job_post is model having qualification as ManyToMany field.
class gjobs(admin.ModelAdmin):
filter_horizontal = ('qualification',)
admin.site.register(govt_job_post, gjobs)
Problem solved. It can save the multiple choice field now.
class GroupAdminForm(forms.ModelForm):
users = forms.ModelMultipleChoiceField(queryset=User.objects.all(),
widget=FilteredSelectMultiple('Users', False),
required=False)
locations = forms.ModelMultipleChoiceField(queryset=Location.objects.all(),
widget=FilteredSelectMultiple('Location', False),
required=False)
class Meta:
model = Group
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance', None)
if instance is not None:
initial = kwargs.get('initial', {})
initial['users'] = instance.user_set.all()
initial['locations'] = instance.c_locations.all()
kwargs['initial'] = initial
super(GroupAdminForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
group = super(GroupAdminForm, self).save(commit=commit)
if commit:
group.user_set = self.cleaned_data['users']
group.locations = self.cleaned_data['locations']
else:
old_save_m2m = self.save_m2m
def new_save_m2m():
old_save_m2m()
group.user_set = self.cleaned_data['users']
group.location_set = self.cleaned_data['locations']
self.save_m2m = new_save_m2m
return group
Either I am overlooking something that makes your situation unusual or you are making it harder than it needs to be. Since you're using the admin, the vast majority of the code necessary to use the admin's more convenient multiselects is already available. All you should need to do is declare your ManyToMany fields, as you have, and then include those fields in your admin class's filter_horizontal attribute. Or filter_vertical if you want the boxes stacked, but your screenshot shows the horizontal case.
This by itself does not require a custom form for your admin.