django modelform use normal field for relation - django

I am building a little message system with this model:
class Mail(models.Model):
sender = models.ForeignKey(Character, related_name="+")
to = models.ForeignKey(Character, related_name="+")
sent_at = models.DateTimeField(auto_now_add=True)
subject = models.CharField(max_length=127)
body = models.TextField()
now i made a modelform from this:
class ComposeForm(forms.ModelForm):
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at"]
however this gives my "to" field a drop down list with all possible characters.
Id like to make this in a normal charfield (later with auto completer) instead of this drop down.
Any idea how i can achieve this?

I've been in a similar place and I found 2 solutions, depending on the needs. The first one is suposing you're going to use something like a select2 and get the query via ajax:
class ComposeForm(forms.ModelForm):
to_char = forms.CharField(max_length=255, required=False) # use the name you want
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at","to"]
So to_char is empty, and then you manage that field as you want, and when you do the POST, you'll get the value of to_char in the view, and assign to the model where you need it.
The other option I suggest is to use a ModelChoiceField instead of Charfield like this:
class ComposeForm(forms.ModelForm):
to_char = forms.ModelMultipleChoiceField(queryset=Character.objects.all())
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at","to"]
This will make easier to use an external select tool as select2 (without AJAX)
I choose the first one when the model has thousands of possible choices, so the select hasn't to load all the choices and the page will be fast. I use a select2 in the template over this field, in this case to_char, that loads the options in an AJAX view
I use the second one when there are hundred of choices and using the autocomplete of select2 over this field has no problems, I think if you don't have too many choices this will be the best for you, the ModelChoiceField, you can attach an autocomplete without any trouble

I might have found even a neater solution to this problem. By overriding the default form field in this way:
class ComposeForm(forms.ModelForm):
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at"]
widgets = {'to': forms.TextInput()}

1) If you do not want to display all the values, then all you need to do is to override to field queryset.
Like:
class ComposeForm(forms.ModelForm):
class Meta:
model = Mail
exclude = ["folder", "sender", "sent_at"]
def __init__(self, *args, **kwargs):
super(ComposeForm, self).__init__(*args, **kwargs)
self.fields['to'].queryset = Character.objects.none()
2) If you want to add autocomplete then you will need view that does the filtering and returns filtered options. You also need some kind of js widget. There are many of those available. On django side you only need to update field widget parameters so your js can pick up the field.

Related

Can I override default CharField to ChoiceField in a ModelForm?

I have a (horrible) database table that will be imported from a huge spreadsheet. The data in the fields is for human consumption and is full of "special cases" so its all stored as text. Going forwards, I'd like to impose a bit of discipline on what users are allowed to put into some of the fields. It's easy enough with custom form validators in most cases.
However, there are a couple of fields for which the human interface ought to be a ChoiceField. Can I override the default form field type (CharField)? (To clarify, the model field is not and cannot be constrained by choices, because the historical data must be stored. I only want to constrain future additions to the table through the create view).
class HorribleTable( models.Model):
...
foo = models.CharField( max_length=16, blank=True, ... )
...
class AddHorribleTableEntryForm( models.Model)
class Meta:
model = HorribleTable
fields = '__all__' # or a list if it helps
FOO_CHOICES = (('square', 'Square'), ('rect', 'Rectangular'), ('circle', 'Circular') )
...?
Perhaps you could render the forms manually, passing the options through the context and make the fields in html.
Take a look at here:https://docs.djangoproject.com/en/4.0/topics/forms/#rendering-fields-manually
I think you can easily set your custom form field as long it will match the data type with the one set in your model (e.g. do not set choices longer than max_length of CharField etc.). Do the following where foo is the same name of the field in your model:
class AddHorribleTableEntryForm(forms.ModelForm):
foo = forms.ChoiceField(choices=FOO_CHOICES)
class Meta:
model = HorribleTable
...
I think this is perfectly fine for a creation form. It's will not work for updates as the values in the DB will most probably not match your choices. For that, I suggest adding a second form handling data updates (maybe with custom permission to restrict it).
UPDATE
Another approach will be to override the forms init method. That way you can handle both actions (create and update) within the same form. Let the user select from a choice field when creating an object. And display as a normal model field for existing objects:
class AddHorribleTableEntryForm(forms.ModelForm):
foo = forms.ChoiceField(choices=FOO_CHOICES)
class Meta:
model = HorribleTable
fields = '__all__' # or a list if it helps
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = kwargs.get("instance", None)
if instance is None:
self.fields["foo"].widget = forms.widgets.Select(choices=self.FOO_CHOICES)

Enable sorting by __str__ column in django admin

I have a django project with a model that looks like:
class Profile(models.Model):
#some other stuff
owner = models.OneToOneField(settings.AUTH_USER_MODEL)
last_modified = models.DateTimeField(default = timezone.now)
def __unicode__(self):
return self.owner.name
__unicode__.admin_order_field = 'owner__last_name'
My model admin looks something like:
class ProfileAdmin(admin.ModelAdmin):
ordering = ['-last_modified']
list_display = ['__unicode__', 'last_modified']
I would like for the admin to be sorted by last_modified by default (as it is now) but to be able to sort alphabetically by clicking on the top of the first column of the list display. I tried to add the __unicode__.admin_order_field line as described here, but that doesn't seem to have made any difference. Is what I want possible? If not why not?
You can only sort fields in the django admin interface if they are fields on your model or if they are fields you custom annotate in the get_queryset method of your ModelAdmin class--essentially fields created at the DB level. However, assuming you are deriving your __unicode__ or __str__ method from some fields on your model (and you are--from owner.name) you should be able to reference those fields and make the column sortable like so (though you could use this method to make the unicode field sortable on any model attribute you'd like):
class ProfileAdmin(admin.ModelAdmin):
def sortable_unicode(self, obj):
return obj.__unicode__()
sortable_unicode.short_description = 'Owner Name'
sortable_unicode.admin_order_field = 'owner__last_name'
ordering = ['-last_modified']
list_display = ['sortable_unicode', 'last_modified']
Though I do find it a bit strange that you will be displaying the owner's name but sorting on last_name. This might be a bit puzzling when you wonder why your sort order doesn't match the displayed name in the admin interface.

Django Country/City dynamic selection : how to allow field value out of queryset in Modelfield?

I am experiencing a problem with Django, trying to build a very simple (shame on me...) thing : I want to filter a ForeignKey ModelField, using another SelectField (with Ajax, for example : A "Country" selectbox autocompletes a "City" selectbox so the user can select his city).
My models look like :
class Country(models.Model):
...
class Town(models.Model):
country = models.ForeignKey(Country)
...
class Person(models.Model):
user = models.ForeignKey(User, unique=True)
town = models.ForeignKey(Town, related_name='persons')
...
Right.
Now I want a form to create/update a person, allowing the user to set a city in a selectbox filtered by the value of another selectbox (Country).
Here is the form I created :
class LazyChoiceField(forms.ChoiceField):
def __init__(self, required=True, widget=None, label=None,
initial=None, help_text=None, *args, **kwargs):
super(LazyChoiceField, self).__init__(required=required, widget=widget,
label=label, initial=initial,
help_text=help_text, *args, **kwargs)
def valid_value(self, value):
return True
class PersonForm(ModelForm):
country = ModelChoiceField(queryset=Country.objects.all())
town = LazyChoiceField(choices=[('0', "Chose a country firts!")])
class Meta:
model = Person
exclude = ('user', 'town', )
widgets = {
'country': forms.Select(attrs={'onchange':'get_towns();'}),
}
It works (providing a ajax.py file, serving a get_towns() function called via Ajax by Jquery script + a bit of JS stuff on the template page). But I am not really satisfied with this way of doing: excluding town field and adding another town LazyChoiceField doesn't seem DRY, does it?
My question is: is it possible to tell Django to not generate a validation error on a ModelField when the value selected is not in the initial queryset (actually, I can't render thousands of towns on the form !).
This feature (selecting Country/town) is very popular on the Web, but I didn't manage to find a simple Django snippet to do such a thing. What do you think about the solution I'm trying to develop? How to do this better and following the DRY principle?
If I don't exclude town from the ModelForm, my 30000 town selectbox makes the page really slow (and unusable)!
Thank you very much in advance!
Glad you solved your issue. Just in case anyone else happens by, Django builds validation for selects against the choices passed in during instantiation. If you're going to do AJAX filtering of selects, your choices as far as Django is concerned must be every allowable choice, unfiltered. Use javascript to make the choices whatever you like after the page is rendered.

Django Form with no required fields

I want to make a form used to filter searches without any field being required. For example given this code:
models.py:
class Message(models.Model):
happened = models.DateTimeField()
filename = models.CharField(max_length=512, blank=True, null=True)
message = models.TextField(blank=True, null=True)
dest = models.CharField(max_length=512, blank=True, null=True)
fromhost = models.ForeignKey(Hosts, related_name='to hosts', blank=True, null=True)
TYPE_CHOICES = ( (u'Info', u'Info'), (u'Error', u'Error'), (u'File', u'File'), (u'BPS', u'BPS'),)
type = models.CharField(max_length=7, choices=TYPE_CHOICES)
job = models.ForeignKey(Jobs)
views.py:
WHEN_CHOICES = ( (u'', ''), (1, u'Today'), (2, u'Two days'), (3, u'Three Days'), (7, u'Week'),(31, u'Month'),)
class MessageSearch(ModelForm): #Class that makes a form from a model that can be customized by placing info above the class Meta
message = forms.CharField(max_length=25, required=False)
job = forms.CharField(max_length=25, required=False)
happened = forms.CharField(max_length=14, widget=forms.Select(choices=WHEN_CHOICES), required=False)
class Meta:
model = Message
That's the code I have now. As you can see it makes a form based on a model. I redefined message in the form because I'm using an icontains filter so I didn't need a giant text box. I redefined the date mostly because I didn't want to have to mess around with dates (I hate working with dates! Who doesnt?) And I changed the jobs field because otherwise I was getting a drop down list of existing jobs and I really wanted to be able to search by common words. So I was able to mark all of those as not required
The problem is it's marking all my other fields as required because in the model they're not allowed to be blank.
Now in the model they can't be blank. If they're blank then the data is bad and I don't want it in the DB. However the form is only a filter form on a page to display the data. I'm never going to save from that form so I don't care if fields are blank or not. So is there an easy way to make all fields as required=false while still using the class Meta: model = Message format in the form? It's really handy that I can make a form directly from a model.
Also this is my first serious attempt at a django app so if something is absurdly wrong please be kind :)
You can create a custom ModelForm that suit your needs. This custom ModelForm will override the save method and set all fields to be non-required:
from django.forms import ModelForm
class SearchForm(ModelForm):
def __init__(self, *args, **kwargs):
super(SearchForm, self).__init__(*args, **kwargs)
for key, field in self.fields.iteritems():
self.fields[key].required = False
So you could declare your forms by simply calling instead of the ModelForm, e.g.:
class MessageForm(SearchForm):
class Meta:
model = Message
You could also pass empty_permitted=True when you instantiate the form, e.g.,
form = MessageSearch(empty_permitted=True)
that way you can still have normal validation rules for when someone does enter data into the form.
I would give a try to the django-filter module :
http://django-filter.readthedocs.io/en/develop/
fields are not required. these are filters actually. It would look like this :
import django_filters
class MessageSearch(django_filters.FilterSet):
class Meta:
model = Message
fields = ['happened', 'filename', 'message', '...', ]
# django-filter has its own default widgets corresponding to the field
# type of the model, but you can tweak and subclass in a django way :
happened = django_filters.DateFromToRangeFilter()
mandatory, hidden filters can be defined if you want to narrow a list of model depending on something like user rights etc.
also : setup a filter on a 'reverse' relationship (the foreignkey is not in the filtered model : the model is referenced elsewhere in another table), is easy, just name the table where the foreign key of the filtered model field is :
# the 'tags' model has a fk like message = models.ForeignKey(Message...)
tags= django_filters.<some filter>(name='tags')
quick extendable and clean to setup.
please note I didn't wrote this module, I'm just very happy with it :)

Validating an Autocomplete field in Django

I have models similar to the following:
class Band(models.Model):
name = models.CharField(unique=True)
class Event(models.Model):
name = models.CharField(max_length=50, unique=True)
bands = models.ManyToManyField(Band)
and essentially I want to use the validation capability offered by a ModelForm that already exists for Event, but I do not want to show the default Multi-Select list (for 'bands') on the page, because the potential length of the related models is extremely long.
I have the following form defined:
class AddEventForm(ModelForm):
class Meta:
model = Event
fields = ('name', )
Which does what is expected for the Model, but of course, validation could care less about the 'bands' field. I've got it working enough to add bands correctly, but there's no correct validation, and it will simply drop bad band IDs.
What should I do so that I can ensure that at least one (correct) band ID has been sent along with my form?
For how I'm sending the band-IDs with auto-complete, see this related question: Django ModelForm Validate custom Autocomplete for M2M, instead of ugly Multi-Select
You can override the default fields in a ModelForm.
class AddEventForm(forms.ModelForm):
band = forms.CharField(max_length=50)
def clean_band(self):
bands = Band.objects.filter(name=band,
self.data.get('band', ''))
if not bands:
raise forms.ValidationError('Please specify a valid band name')
self.cleaned_data['band_id'] = bands[0].id
Then you can use your autocomplete widget, or some other widget. You can also use a custom widget, just pass it into the band field definition: band = forms.CharField(widget=...)