I have a django (1.11.9) ModelForm for events with times (start and end). When I display it to the user in a template I want last_field to be the last field in the form at the bottom:
class EventForm(forms.ModelForm):
dt = '%Y-%m-%d %H:%M'
start = forms.DateTimeField(
label="Start time (in event's local time)",
required=False,
input_formats=[dt]
)
end = forms.DateTimeField(
label="End time (in event's local time)",
required=False,
input_formats=[dt]
)
class Meta:
model = Event
fields = [
'name', 'flexibility', 'timezone',
'date_start', 'date_end', 'last_field',
]
def __init__(self, *args, **kwargs):
super(EventForm, self).__init__(*args, **kwargs)
self.fields['date_start'].widget = forms.HiddenInput()
self.fields['date_end'].widget = forms.HiddenInput()
i = kwargs.get('instance')
tz = pytz.timezone(i.timezone) if i and i.timezone else None
if i and i.flexibility == Event.FLEXIBILITY_ONTIME and tz:
self.fields['start'].initial = i.date_start.astimezone(tz).strftime(self.dt) if i.date_start else None
self.fields['end'].initial = i.date_end.astimezone(tz).strftime(self.dt) if i.date_end else None
else:
self.fields['start'].initial = None
self.fields['end'].initial = None
however, even explicitly setting 'last_field' at the end of the fields = [] array (per docs https://docs.djangoproject.com/en/1.11/topics/forms/modelforms/#changing-the-order-of-fields), the start and end widget fields in the form render below it out of the set field order.
is there a way to override the widgets to force the ordering of the fields? thanks
Related
I've been scratching my head over this for the last little while. I have been able to change the modelfield's field queryset and widget attributes, well somewhat!
class InvoiceItemForm(ModelForm):
UOM = forms.ChoiceField (choices = site_defaults.UOM)
class meta:
model = InvoiceItem
fields = ['name', 'costcode', 'rate', 'quantity',]
labels = {'name': 'Item', 'rate': 'Cost Per Unit', 'quantity': 'Base Quantity'}
widgets = {'UOM': forms.Select(choices = site_defaults.UOM )}
def __init__(self, current_user, current_project, *args, **kwargs):
''' Rendering custom ModelForm '''
super(InvoiceItemForm, self).__init__(*args, **kwargs)
the_title = None
the_instance = kwargs.get('instance', None)
if the_instance:
the_costcode = the_instance.costcode
if the_costcode:
the_title = the_costcode.title
self.fields['costcode'].queryset = CostCode.objects.filter(project = current_project, item = 0)
self.fields['costcode'].widget = forms.TextInput(attrs={'class': 'site-flex-select-large', 'value': the_title})
When this is rendered, the costcode field takes the right instance. Also, the class is shown as site-flex-select-large, but the title is shown as the instance.id and not the_title which is the instance.title (a text field is displayed with value of 192 instead of the title of the invoice item).
Why is Django ignoring some changes and accepting some other changes to the field?
I'm not sure if it is a relevant detail or not, but the modelform is used in an inlineformset:
expenses_forms = self.InvoiceItem_InlineFormSet(instance = the_invoice, prefix='expenses', form_kwargs={'current_user': user, 'current_project': project})
A fields widget is not the place that you should be setting initial values for fields. You should set this in the "initial" kwarg to the form's __init__ method, you can pass it to the call to super. You then can set the costcode widget in the Meta
class InvoiceItemForm(ModelForm):
UOM = forms.ChoiceField (choices = site_defaults.UOM)
class Meta:
model = InvoiceItem
fields = ['name', 'costcode', 'rate', 'quantity',]
labels = {'name': 'Item', 'rate': 'Cost Per Unit', 'quantity': 'Base Quantity'}
widgets = {
'UOM': forms.Select(choices = site_defaults.UOM ),
'costcode': forms.TextInput(attrs={'class': 'site-flex-select-large'})
}
def __init__(self, current_user, current_project, *args, **kwargs):
the_instance = kwargs.get('instance', None)
if the_instance:
the_costcode = the_instance.costcode
if the_costcode:
initial = kwargs.get('initial', {})
initial['costcode'] = the_costcode.title
kwargs['initial'] = initial
super(InvoiceItemForm, self).__init__(*args, **kwargs)
EDIT: like Willem says, the costcode field is a TextInput so it does not make sense to set a queryset attribute on it unless you change it to a select
The value is not taken from the attrs, it is taken from the value of that field. You can set the .initial attribute of the field, like:
def __init__(self, current_user, current_project, *args, **kwargs):
''' Rendering custom ModelForm '''
super(InvoiceItemForm, self).__init__(*args, **kwargs)
the_title = None
the_instance = kwargs.get('instance', None)
if the_instance:
the_costcode = the_instance.costcode
if the_costcode:
the_title = the_costcode.title
self.fields['costcode'].queryset = CostCode.objects.filter(project=current_project, item=0)
self.fields['costcode'].initial = the_title
self.fields['costcode'].widget = forms.TextInput(attrs={'class': 'site-flex-select-large'})
That being said, by using a TextInput, it will, as far as I know, just ignore the queryset, and it will not properly validate the data. I think you better use a Select widget [Django-doc] here, and then use some CSS/JavaScript to make it searchable through text.
I've got the following Form. I let Django render the form automatically for me in my template with: {{ form.as_p() }}. As you can see I've got the company field, however, it's redundant as I am setting the company by clean_company. The company field is hidden, but I want it to be completely gone in the template. I still need it in the form though, because I want to be able to call: form.save(commit=True).
Is there a way to get the hidden field out of my template?
class PlantPurchaseForm(forms.ModelForm):
company = forms.CharField(initial="", widget=forms.HiddenInput())
number = forms.IntegerField(initial=10000, min_value=10000, max_value=99999)
date = forms.DateField(widget=forms.DateInput(attrs={"type": "date"}))
class Meta:
model = PlantPurchase
fields = (
"company",
"number",
"date",
"plant",
"costs",
)
def __init__(self, company, *args, **kwargs):
self.company = company
super(PlantPurchaseForm, self).__init__(*args, **kwargs)
def clean_company(self):
data = self.cleaned_data["company"]
data = self.company
return data
def clean_date(self):
data = self.cleaned_data["date"]
data = datetime.combine(data, time(hour=12))
data = pytz.utc.localize(data)
return data
The PlantPurchase Model:
class PlantPurchase(models.Model):
company = models.ForeignKey(Company, related_name="plant_purchases")
number = models.PositiveSmallIntegerField(unique=True, validators=[MinValueValidator(10000),
MaxValueValidator(99999)])
date = models.DateTimeField()
plant = models.ForeignKey(Plant, related_name="plant_purchase", on_delete=models.PROTECT)
costs = models.DecimalField(max_digits=8, decimal_places=2)
class Meta:
unique_together = ("company", "number")
def __str__(self):
text = "Purchase: #{} {}".format(self.number, self.plant)
return text
I solved my problem by accessing the form instance in the init method. This way I could remove the company field and it won't be rendered in the template anymore.
class PlantPurchaseForm(forms.ModelForm):
number = forms.IntegerField(initial=10000, min_value=10000, max_value=99999)
date = forms.DateField(widget=forms.DateInput(attrs={"type": "date"}))
class Meta:
model = PlantPurchase
fields = (
"number",
"date",
"plant",
"costs",
)
def __init__(self, company, *args, **kwargs):
super(PlantPurchaseForm, self).__init__(*args, **kwargs)
self.instance.company = company
def clean_date(self):
data = self.cleaned_data["date"]
data = datetime.combine(data, time(hour=12))
data = pytz.utc.localize(data)
return data
There are several ways how to do it.
Basically, if you are sure that hidden field is not in use then you can remove it from the form with exlude. You can remove init method as well.
class PlantPurchaseForm(forms.ModelForm):
number = forms.IntegerField(initial=10000, min_value=10000, max_value=99999)
date = forms.DateField(widget=forms.DateInput(attrs={"type": "date"}))
class Meta:
model = PlantPurchase
fields = [
'company',
'number',
'date',
'plant',
'costs',
]
exclude = ["company"]
After that you need to save company into the models instance. You can either save it after form is validated:
def post(self, request):
...
if form.is_valid():
plantPurchase = form.save()
plantPurchase.company = company
plantPurchase.save()
...
..or pass the company into the save method of the form:
def save(self, company, force_insert=False, force_update=False, commit=False):
purchase = super(PlantPurchaseForm, self).save(commit=commit)
purchase.company = company
purchase.save()
return purchase
I have a admin master detail, using tabular inline forms.
I have some special validations to accomplish:
If "field_type" is "list", validate that at least one item in the formset has been added.
But if not (field_type has another value), do not validate.
If "field_type" is "list", then, make visible the formset, otherwise hide it. This is javascript. I also must validate that on the server. I do that on the clean() of ValueItemInlineFormSet. The problem is that is now validating formset always and it should just happen when field_type = "list". How can I get the value of my master field into the formset?
class ValueItemInlineFormSet(BaseInlineFormSet):
def clean(self):
"""Check that at least one service has been entered."""
super(ValueItemInlineFormSet, self).clean()
if any(self.errors):
return
print(self.cleaned_data)
if not any(cleaned_data and not cleaned_data.get('DELETE', False) for cleaned_data in self.cleaned_data):
raise forms.ValidationError('At least one item required.')
class ValueItemInline(admin.TabularInline):
model = ValueItem
formset = ValueItemInlineFormSet
class MySelect(forms.Select):
def render_option(self, selected_choices, option_value, option_label):
if option_value is None:
option_value = ''
option_value = force_text(option_value)
data_attrs = self.attrs['data_attrs']
option_attrs = ''
if data_attrs and option_value:
obj = self.choices.queryset.get(id=option_value)
for attr in data_attrs:
attr_value = getattr(obj, attr)
option_attrs += 'data-{}={} '.format(attr, attr_value)
if option_value in selected_choices:
selected_html = mark_safe(' selected="selected"')
if not self.allow_multiple_selected:
# Only allow for a single selection.
selected_choices.remove(option_value)
else:
selected_html = ''
return format_html('<option value="{}" {}{}>{}</option>', option_value, option_attrs, selected_html, force_text(option_label))
class RequirementFieldForm(forms.ModelForm):
field_type = forms.ModelChoiceField(queryset=FieldType.objects.all(),
widget=MySelect(attrs={'data_attrs': ('identifier', 'name')}))
def __init__(self, *args, **kwargs):
self.qs = FieldType.objects.all()
super(RequirementFieldForm, self).__init__(*args, **kwargs)
class Meta:
model = RequirementField
fields = ['field_type', 'name', 'description', 'identifier', 'mandatory', 'order_nr', 'active']
my model:
class Event(models.Model):
title = models.CharField(max_length=255)
start = models.DateTimeField()
end = models.DateTimeField()
theme = models.ForeignKey(Theme)
class Theme(models.Model):
name = models.CharField(max_length=100)
color = models.CharField(max_length=50)
text_color = models.CharField(max_length=50)
my form:
class EventForm(ModelForm):
class Meta:
model = Event
fields = ['title', 'start', 'end']
theme = forms.ModelChoiceField(
queryset=Theme.objects.filter(public=True),
empty_label='None'
)
my view:
#login_required
def index(request):
if request.method == 'POST':
form = EventForm(request.POST)
if form.is_valid():
form.save()
Now If I fill in the values in the form star, end, title and select a theme from a list that django creates for me I get an error when I try to run the form.save() method.
IntegrityError: null value in column "theme_id" violates not-null constraint
But when I look into form.cleaned_data I can see that in theme is an instance of my Theme model available.
you cannot save Event without Theme object, so you need something like
form = EventForm(request.POST)
if form.is_valid():
# get your Theme object 'your_theme_object'
event = form.save(commit=False)
event.theme = your_theme_object
event.save()
I should have commented but I don't have enough point.
I think better way to achieve this thing is:
class EventForm(ModelForm):
class Meta:
model = Event
fields = ['title', 'start', 'end', 'theme']
As 'theme' is foreign key to Event Model, it'll appear as drop down on your template.
As here you want to filter theme objects, you can achieve it by overriding init :
class EventForm(ModelForm):
def __init__(self, *args, **kwargs):
super(EventForm, self).__init__(*args, **kwargs)
self.fields['theme'].queryset = self.fields['theme'].queryset.filter(public=True)
class Meta:
model = Event
fields = ['title', 'start', 'end', 'theme']
I have one big object - report of users trips. I have provided interface for user to edit some fields in that report one by one. Since the user is can edit one field at a time i have created method which creates modelform based on which field user wants to edit.
It works like that:
def createFieldEditorForm(client, field, report_id, request = None):
from django.shortcuts import get_object_or_404
report_instance = get_object_or_404(DriveReport, id = report_id)
class FieldEditorForm(forms.ModelForm):
class Meta:
model = DriveReport
fields = ['id', ]
id = forms.IntegerField(label = _(u"ride_id"), widget=forms.HiddenInput())
def __init__(self, *args, **kwargs):
super(FieldEditorForm, self).__init__(*args, **kwargs)
self.build_fields()
def build_fields(self):
if field == 'driver_id':
driver_query = Driver.objects.filter(client = client)
choices = [('', u'------'),]
for driver in driver_query:
choices.append((driver.id, driver.name))
self.fields[field] = forms.IntegerField(
label = _(u"Drivers"),
widget = forms.Select(
choices = choices,
attrs = {'data-name':'driver_id'} ),
required = False)
elif field == 'project_id':
area_query = Area.objects.filter(client = client)
choices = [('', u'------'),]
for area in area_query:
choices.append((area.id, area.name))
self.fields[field] = forms.IntegerField(
label = _(u"Projects"),
widget = froms.Select(
choices = choices,
attrs = {'data-name':'project_id'} ),
required = False)
elif field == 'trip_type':
self.fields[field] = forms.CharField(
label = _(u"Projects"),
widget = forms.Select(
choices = [(key, value) for key, value in TRIP_TYPE_CHOICES.iteritems()],
attrs = {'data-name':'trip_type'} ),
required = False)
else:
self.fields[field] = forms.CharField(required = False, widget = forms.TextInput(attrs = {'data-name':field} ))
if request is None:
return FieldEditorForm(instance = report_instance)
else:
return FieldEditorForm(request, instance = report_instance)
And in view
it goes like that:
if request.method == 'POST':
form = createFieldEditorForm(activeaccount, field_id, ride_id, request.POST)
if form.is_valid():
form.save()
messages.success(request, _("New field value successfully added/updated"))
else:
messages.error(request, _("Value was not saved"))
return feedback_to_json(request, form)
and the outcome is - i get success message in browser, but the field is not updated.
i tried overwriting form.save() method and checking if self.cleaned_data contains [field] - and it does. Its right there. Even if i print out form before save and after save i can see, that the data has reached backend and its all nice and neat.. but the damn value is just not saved/updated into database
The save functionality of a ModelForm only works on the fields actually in the fields attribute. Since you're dynamically adding fields, you'll need to save those manually (save the form with commit=False and then add the data to the unsaved object that's returned).