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)
Related
I am trying to find out an efficient way to set a field value within form init method. My models are similar to below
class Users(models.Model):
firstname = models.CharField()
lastname = models.CharField()
class profile(models.model):
user = models.ForeignKey(Users, on_delete=models.PROTECT)
class logindetails(models.model):
user = models.ForeignKey(Users, on_delete=models.PROTECT)
profile = models.ForeignKey(profile, on_delete=models.PROTECT)
login_time = models.DateField(auto_now=True)
My form is like as below:
class LoginForm(forms.ModelForm):
class Meta:
model = logindetails
fields = [__all__]
def __init__(self, *args, **kwargs):
self._rowid = kwargs.pop('rowid', None)
super(LoginForm, self).__init__(*args, **kwargs)
instance = profile.objects.get(id=self._rowid)
self.fields['user'] = instance.user <--- Facing difficulties here
Any help will be appreciated.
Django had built-in ways of setting initial form values, the documentation is available here: https://docs.djangoproject.com/en/3.0/ref/forms/api/#dynamic-initial-values
I have the (simple, I suppose) need of having a situation like so: there are many profiles, and there are many ensembles, and each profile has to be able to be part of one or more ensembles. This is my code:
class Ensemble(models.Model):
ensembleName = models.CharField(max_length=200)
members = models.ManyToManyField('Profile', related_name='members')
def __str__(self):
return self.ensembleName
class Profile(models.Model):
ensemble = models.ForeignKey(Ensemble, on_delete=models.CASCADE, blank=True, null=True)
[...]
It all works well, but to an extent. From the Django administration I can, from the 'ensemble' page, select its members. I can also select, from the 'profile' page, which ensembles that profile belongs. The issue is: they are not synchronised: if I add a profile to an ensemble via the 'profile' page this is not reflected in the 'ensemble' page and the other way round, i.e. in the 'profiles details' page I don't see the ensemble to which I previously assigned that profile from the 'ensemble' page.
My form
class ProfileUpdateForm(forms.ModelForm):
class Meta:
model = Profile
fields = ('image', 'role', 'skills', 'gender', etc...)
class EnsemblesForm(forms.ModelForm):
class Meta:
model = Ensemble
fields = ('ensemble_name',)
def __init__(self, *args, **kwargs):
super(EnsemblesForm, self).__init__(*args, **kwargs)
self.fields['ensemble_name'].queryset = (obj for obj in Ensemble.objects.all()) #This doesn't output anything
I am trying to create a Model and ModelForm with "name" and "client" fields that have the following cleaning and validation characteristics. I can manage each individual requirement but can't seem get them to work together.
An authenticated user can enter a name for an Item
Item is saved with the name and forced to the client that is associated with the user account.
Name is cleaned via ' '.join(name.strip().split())
Name is validated so that (cleaned_name.lower(),client) is unique
EG: If "FOO BAR" exists in the user's associated client, user would get an error if they enter "foo bar"
It is a fairly simple model:
class Item(BaseModel):
class Meta:
unique_together = (("client", "name"),)
client = models.ForeignKey(Client,related_name='items',null=True,blank=False)
name = models.CharField(max_length=64, null=False, blank=False)
def clean_name(self):
return ' '.join(self.cleaned_data['name'].strip().split())
All item creates/updates are done via Django REST Framework:
class ItemSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Item
fields = ('id','name')
def create(self,validated_data):
item = Item.objects.create(name=validated_data['name'],client=self.context['request'].user.client)
item.save()
return item
I would prefer as much of the logic in the Model as possible (eg, not use SQL to create indexes), but could push some of the validation to the serializer if need be.
Tx.
I ended up with the following. The only caveat is that I have to include a name_slug field to store for sorting purposes.
models.py
class Item(BaseModel):
class Meta:
db_table = 'item'
ordering = ['name_slug']
# relations
client = models.ForeignKey(Client,related_name='items',null=True,blank=False)
# attributes
name = models.CharField(max_length=64, null=False, blank=False)
name_slug = models.CharField(max_length=64, null=False, blank=True)
def clean(self):
self.name = ' '.join(self.name.strip().split())
if Item.objects.filter(client=self.client,name__iexact=self.name).count() > 0:
raise ValidationError({'name': 'Name already exists. Please enter a different name'})
def save(self, *args, **kwargs):
self.name_slug = '-'.join(self.name.split()).lower()
super(Item, self).save(*args, **kwargs)
serializers.py
class ItemSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Item
fields = ('id','name','name_slug')
read_only_fields = ('name_slug',)
def validate(self, attrs):
attrs['client'] = self.context['request'].user.client
instance = Item(**attrs)
instance.clean()
return { k: getattr(instance,k) for k in attrs.keys()}
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.
I'm trying to use CBVs as much as possible and want to pre-populate data in a ModelForm based on a generic.CreateView with some data passed in via URL.
I might be over thinking or confusing myself. All code abridged for legibility
We have an inventory system with PartNumbers (abstractions), Carriers (actual instances of PartNumbers with location, serial and quantity numbers) and Movements for recording when items are extracted from the inventory, how much is taken and what Carrier it came from.
I would like to have the "extract inventory" link on the PartNumber detail page, and then have the available carriers ( pn.carrier_set.all() ) auto filled into the FK drop down on the MovementForm.
models.py
class PartNumber(models.Model):
name = models.CharField("Description", max_length=100)
supplier_part_number = models.CharField(max_length=30, unique=True)
slug = models.SlugField(max_length=40, unique=True)
class Carrier(models.Model):
part_numbers = models.ForeignKey(PartNumber)
slug = models.SlugField(max_length=10, unique=True, blank=True, editable=False)
location = models.ForeignKey(Location)
serial_number = models.CharField(max_length=45, unique=True, null=True, blank=True)
qty_at_new = models.IntegerField()
qty_current = models.IntegerField()
class Movement(models.Model):
carrier = models.ForeignKey(Carrier)
date = models.DateField(default=timezone.now())
qty = models.IntegerField()
I have been playing around with get_initial() and get_form_kwargs() without success:
In urls.py I collect the PartNumber via url as pn_slug
url(r'^partnumber/(?P<pn_slug>[-\w]+)/extract/$', views.MovementCreate.as_view(), name='pn_extract'),
forms.py is generic
class MovementForm(forms.ModelForm):
class Meta:
model = Movement
views.py
class MovementCreate(generic.CreateView):
form_class = MovementForm
model = Movement
def get_form_kwargs(self):
kwargs = super(MovementCreate, self).get_form_kwargs()
kwargs['pn_slug'] = self.request.POST.get("pn_slug")
return kwargs
# here we get the appropriate part and carrier and.
# return it in the form
def get_initial(self):
initial = super(MovementCreate, self).get_initial()
# this didn't work, hence using get_form_kwargs
#pn = PartNumber.objects.get(slug=self.request.POST.get("pn_slug"))
pn = PartNumber.objects.get(slug=self[pn_slug])
carriers = pn.carrier_set.all()
initial['carrier'] = carriers
return initial
As it stands, I'm getting "global name 'pn_slug' is not defined" errors - but I doubt that error accurately reflects what I have done wrong.
I have been using these posts as rough guidelines:
How to subclass django's generic CreateView with initial data?
How do I use CreateView with a ModelForm
If I understand you correctly from our comments, all you need is just to change the queryset of the MovementForm's carrier field to set the available options. In that case, I would use get_initial nor get_form_kwargs at all. Instead, I would do it in get_form:
def get_form(self, *args, **kwargs):
form = super(MovementCreate, self).get_form(*args, **kwargs)
pn = PartNumber.objects.get(slug=self.kwargs['pn_slug'])
carriers = pn.carrier_set.all()
form.fields['carrier'].queryset = carriers
return form
Another way to do it would be to use get_form_kwargs:
def get_form_kwargs(self):
kwargs = super(MovementCreate, self).get_form_kwargs()
kwargs['pn_slug'] = self.kwargs.get("pn_slug")
return kwargs
Then, in the form's __init__, set the queryset:
class MovementForm(forms.ModelForm):
class Meta:
model = Movement
def __init__(self, *args, **kwargs):
pn_slug = kwargs.pop('pn_slug')
super(MovementForm, self).__init__(*args, **kwargs)
pn = PartNumber.objects.get(slug=pn_slug)
carriers = pn.carrier_set.all()
self.fields['carrier'].queryset = carriers
Personally, I would prefer the first method as it is less code.