Store form fields as key-values / individual rows - python

I have a simple form in Django that looks like this:
class SettingForm(forms.Form):
theme = forms.CharField(rrequired=True,
initial='multgi'
)
defaultinputmessage = forms.CharField(required=True,
initial='Type here to begin..'
)
...and the model to store it looks like:
class Setting(models.Model):
name = models.CharField(
null=False, max_length=255
)
value= models.CharField(
null=False, max_length=255
)
When the form is submitted, how can i store the form fields as key value pairs and then when the page is rendered, how can I initialize the form with the key's value. I've tried looking for an implementation of this but have been unable to find one.
Any help?
Thanks.

I'm assuming you want to store 'theme' as the name and the value as the value, same for defaultinputmessage. If that's the case, this should work:
form = SettingForm({'theme': 'sometheme', 'defaultinputmessage': 'hello'})
if form.is_valid():
for key in form.fields.keys():
setting = Setting.objects.create(name=key, value=form.cleaned_data[key])

Here's how I did it.
I needed to do this because I had a Model that stored information as key value pairs and I needed to build a ModelForm on that Model but the ModelForm should display the key-value pairs as fields i.e. pivot the rows to columns. By default, the get() method of the Model always returns a Model instance of itself and I needed to use a custom Model. Here's what my key-value pair model looked like:
class Setting(models.Model):
domain = models.ForeignKey(Domain)
name = models.CharField(null=False, max_length=255)
value = models.CharField(null=False, max_length=255)
objects = SettingManager()
I built a custom manager on this to override the get() method:
class SettingManager(models.Manager):
def get(self, *args, **kwargs):
from modules.customer.proxies import *
from modules.customer.models import *
object = type('DomainSettings', (SettingProxy,), {'__module__' : 'modules.customer'})()
for pair in self.filter(*args, **kwargs): setattr(object, pair.name, pair.value)
setattr(object, 'domain', Domain.objects.get(id=int(kwargs['domain__exact'])))
return object
This Manager would instantiate an instance of this abstract model. (Abstract models don't have tables so Django doesn't throw up errors)
class SettingProxy(models.Model):
domain = models.ForeignKey(Domain, null=False, verbose_name="Domain")
theme = models.CharField(null=False, default='mytheme', max_length=16)
message = models.CharField(null=False, default='Waddup', max_length=64)
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
super(SettingProxy, self).__init__(*args, **kwargs)
for field in self._meta.fields:
if isinstance(field, models.AutoField):
del field
def save(self, *args, **kwargs):
with transaction.commit_on_success():
Setting.objects.filter(domain=self.domain).delete()
for field in self._meta.fields:
if isinstance(field, models.ForeignKey) or isinstance(field, models.AutoField):
continue
else:
print field.name + ': ' + field.value_to_string(self)
Setting.objects.create(domain=self.domain,
name=field.name, value=field.value_to_string(self)
)
This proxy has all the fields that I'd like display in my ModelFom and store as key-value pairs in my model. Now if I ever needed to add more fields, I could simply modify this abstract model and not have to edit the actual model itself. Now that I have a model, I can simply build a ModelForm on it like so:
class SettingsForm(forms.ModelForm):
class Meta:
model = SettingProxy
exclude = ('domain',)
def save(self, domain, *args, **kwargs):
print self.cleaned_data
commit = kwargs.get('commit', True)
kwargs['commit'] = False
setting = super(SettingsForm, self).save(*args, **kwargs)
setting.domain = domain
if commit:
setting.save()
return setting
I hope this helps. It required a lot of digging through the API docs to figure this out.

Related

Perfom Django Validation for field that is NOT part of form

I would like to raise a ValidationError based on one of the fields in my Django model, without having the respective filed as part of a ModelForm. What I found after googling a bit is the concept of validators for models. So I tried to do the following:
def minimumDuration(value):
if value == 0:
raise ValidationError("Minimum value accepted is 1 second!")
class PlaylistItem(models.Model):
position = models.IntegerField(null=False)
content = models.ForeignKey(Content, null=True, on_delete=models.SET_NULL)
item_duration = models.IntegerField(validators = [minimumDuration], default = 5, null=True, blank=True)
playlist = models.ForeignKey(Playlist, null=True, on_delete=models.CASCADE)
However, no error appears when I introduce 0 in the respective field. From Django's documentation I found out that validators are not automatically applied when saving a model. It redirected me to this page, but I don't really understand how to apply those. Any idea?
Here is an example of a form with such a custom field outside of the Model:
class ExampleForm(forms.ModelForm):
custom_field = forms.BooleanField(
label='Just non model field, replace with the type you need',
required=False
)
class Meta:
model = YourModel
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# optional: further customize field widget
self.fields['custom_field'].widget.attrs.update({
'id': self.instance.pk + '-custom_field',
'class': 'custom-field-class'
})
self.fields['custom_field'].initial = self._get_custom_initial()
def _get_custom_initial(self):
# compute initial value based on self.instance and other logic
return True
def _valid_custom_field(value):
# validate your value here
# return Boolean
def clean(self):
"""
The important method: override clean to hook your validation
"""
super().clean()
custom_field_val = self.cleaned_data.get('custom_field')
if not self._valid_custom_field(custom_field_val):
raise ValidationError(
'Custom Field is not valid')

Django Include ManyToManyField on "other" model in ModelForm

I would like to have a form with the preselected checkboxes of a ManyToManyField.
models.py
class Store(models.Model):
...
class Brand(models.Model):
stores = models.ManyToManyField(Store, blank=True, related_name="brands")
forms.py
class StoreForm(ModelForm):
class Meta:
model = Store
fields = ('brands',)
I get this exception:
django.core.exceptions.FieldError: Unknown field(s) (brands) specified for Store
I know that I can add the field manually to the class:
brands = forms.ModelMultipleChoiceField(
queryset=Brand.objects.all(),
widget=forms.CheckboxSelectMultiple,
)
If I do this the checkboxes are not preselected.
How is it possible to include the ManyToMany field from "the other side" of the model (from Store)?
#hedgie To change the field in the other model is not a good option for me because I use it already.
But the __init__() was a good hint. I come up with this solution and it seems to work.
class StoreForm(ModelForm):
def __init__(self, *args, **kwargs):
if kwargs.get('instance'):
brand_ids = [t.pk for t in kwargs['instance'].brands.all()]
kwargs['initial'] = {
'brands': brand_ids,
}
super().__init__(*args, **kwargs)
# https://stackoverflow.com/questions/49932426/save-many-to-many-field-django-forms
def save(self, commit=True):
# Get the unsaved Pizza instance
instance = forms.ModelForm.save(self, False)
# Prepare a 'save_m2m' method for the form,
old_save_m2m = self.save_m2m
def save_m2m():
old_save_m2m()
# This is where we actually link the pizza with toppings
instance.brands.clear()
for brand in self.cleaned_data['brands']:
instance.brands.add(brand)
self.save_m2m = save_m2m
# Do we need to save all changes now?
# Just like this
# if commit:
instance.save()
self.save_m2m()
return instance
brands = forms.ModelMultipleChoiceField(
queryset=Brand.objects.all(),
widget=forms.CheckboxSelectMultiple,
)
Though it seems to be not very elegant. I wonder why django does not support a better way.
One possibility is to define the field on the "other" model. So instead of writing this:
class Store(models.Model):
...
class Brand(models.Model):
stores = models.ManyToManyField(Store, blank=True, related_name="brands")
You can write this:
class Brand(models.Model):
...
class Store(models.Model):
brands = models.ManyToManyField(Brand, blank=True, related_name="stores")
Or, if you have manually added the field to the form, you could populate its initial value in the form's __init__() method.

Django Form field initial value when updating an instance

I have a custom Django ModelForm that I use to update a model instance.
This is the example model:
class MyModel(models.Model):
number = models.CharField(_("Number"), max_length=30, unique=True)
sent_date = models.DateField(_('Sent date'), null=True, blank=True)
When creating an instance I will pass only the number field, that is why I don't want the sent_date to be required.
Then I have a view that updates the sent_date field, using this custom form:
# Generic form updater
class MyModelUpdateForm(forms.ModelForm):
class Meta:
model = MyModel
fields = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make fields mandatory
if hasattr(self, 'required_fields'):
for field_name in self.required_fields:
self.fields[field_name].required = True
# Set initial values
if hasattr(self, 'initial_values'):
for field_name, value in self.initial_values.items():
self.initial[field_name] = value
class SentForm(MyModelUpdateForm):
required_fields = ['sent_date']
initial_values = {'sent_date': datetime.date.today()}
class Meta(MyModelUpdateForm.Meta):
fields = ['sent_date']
field_classes = {'sent_date': MyCustomDateField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
MyModelUpdateForm is a generic ancestor for concrete forms like SentForm.
In my view whenever there is a GET I manually instantiate the form with:
my_form = SentForm({instance: my_model_instance})
So in this case I would expect the sent_date field to have an initial value set to today's date even tough the real model instance field is None.
If I inspect my_form object it does indeed have these attributes:
initial: {'sent_date': datetime.date(2018, 3, 1)}
instance: my_model_instance
fields: {'sent_date':
...: ...,
'initial': None # Why this is None?
...: ...
}
So apparently it should work but it doesn't: the field is always empty.
So I suspect that the value is coming from my_model_instance.sent_date that is in fact None.
The initial['sent_date'] = datetime.date(2018, 3, 1) is correct.
On the other side fields['sent_date']['initial'] = None it's not.
How can I always show the initial value when my_model_instance.sent_date is None?
Apparently I've solved with:
class MyModelUpdateForm(forms.ModelForm):
class Meta:
model = MyModel
fields = []
def __init__(self, *args, **kwargs):
initial = kwargs.get('initial', {})
if hasattr(self, 'initial_values') and not kwargs.get('data'):
for field_name, value in self.initial_values.items():
if not getattr(kwargs.get('instance', None), field_name, None):
initial[field_name] = value
kwargs.update({'initial': initial})
super().__init__(*args, **kwargs)
# Make fields mandatory
if hasattr(self, 'required_fields'):
for field_name in self.required_fields:
self.fields[field_name].required = True
Even tough it works I wouldn't mind a less hackish solution if anyone has any :)
I have this case in many places in my app without having any problem. However, I use a different way to set up initial value of some fields of an existing instance. Instead of:
self.initial[field_name] = value
I write, after having called super():
self.fields[field_name].initial = value
Can you try and tell the result ?

Django - how can I make a cell in a table in the admin *changelist* interface editable only if it is null?

I would like my data to be editable inline in the Django admin page. However, I only want some fields columns in each row to be editable. These columns will change for each row. Basically, I want a dropdown choice to be displayed if the value in a certain cell is null. If it is not null, then I don't want it to be editable and would like it to be readonly.
models.py:
class Size(models.Model):
size = models.CharField(max_length=20, primary_key=True)
class Book(models.Model):
title = models.CharField(max_length=100, primary_key=True)
size = models.ForeignKey(Size, null=False)
class Pamphlet(models.Model):
title = models.CharField(max_length=100, primary_key=True)
size = models.ForeignKey(Size, null=True)
book = models.ForeignKey(Book, null=True)
admin.py:
class PamphletAdmin(admin.ModelAdmin):
model = Pamphlet
list_editable = ('size','book')
list_display = ('title', 'size', 'book',)
def get_changelist_form(self, request, **kwargs):
return PamphletChangeListForm
forms.py:
class PamphletChangeListForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(PamphletChangeListForm, self).__init__(*args, **kwargs)
instance = kwargs.get('instance')
if instance:
self.fields['book'].queryset = Book.objects.filter(
size=instance.size
)
if instance.size is not None:
self.fields['size'].widget.attrs['readonly'] = 'readonly'
This setup is not working for me. The size shows as editable in the changelist form even when it is not null. Also - what have I failed to understand?
If your field uses an input element, such as a TextField, add the readonly attribute to the field's widget's attrs dict in the changelist form's __init__ method. Something like this:
class PamphletChangeListForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(PamphletChangeListForm, self).__init__(*args, **kwargs)
instance = kwargs.get('instance')
if instance:
self.fields['book'].queryset = Book.objects.filter(
size=instance.size
)
if instance.size is not None:
self.fields['size'].widget.attrs['readonly'] = 'readonly'
That won't protect you against a malicious user faking post data - for that you'd need to customize your admin further. But if you have malicious users on your admin you have bigger problems.
If your field uses a select element, you have to change it more - select attributes don't have readonly as a supported attribute. Instead, you'll want a hidden input with the unchanging value, and a text representation so the user can see what the setting is. Django does not include such a widget, but you can define your own:
class LabeledHiddenInput(forms.widgets.HiddenInput):
def render(self, name, value, attrs=None):
base_output = super(LabeledHiddenInput, self).render(name, value, attrs)
if value:
return base_output + unicode(value)
else:
return base_output
You might need more careful escaping or even some HTML formatting, this is just a quick example. Check the source code for the built in widgets if you need more examples.
Then you can use that widget instead of the default select:
if instance.size is not None:
self.fields['size'].widget = LabeledHiddenInput()

Django required field in model form

I have a form where a couple of fields are coming out as required when I don't want them too. Here is the form from models.py
class CircuitForm(ModelForm):
class Meta:
model = Circuit
exclude = ('lastPaged',)
def __init__(self, *args, **kwargs):
super(CircuitForm, self).__init__(*args, **kwargs)
self.fields['begin'].widget = widgets.AdminSplitDateTime()
self.fields['end'].widget = widgets.AdminSplitDateTime()
In the actual Circuit model, the fields are defined like this:
begin = models.DateTimeField('Start Time', null=True, blank=True)
end = models.DateTimeField('Stop Time', null=True, blank=True)
My views.py for this is here:
def addCircuitForm(request):
if request.method == 'POST':
form = CircuitForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect('/sla/all')
form = CircuitForm()
return render_to_response('sla/add.html', {'form': form})
What can I do so that the two fields aren't required?
If you don't want to modify blank setting for your fields inside models (doing so will break normal validation in admin site), you can do the following in your Form class:
def __init__(self, *args, **kwargs):
super(CircuitForm, self).__init__(*args, **kwargs)
for key in self.fields:
self.fields[key].required = False
The redefined constructor won't harm any functionality.
If the model field has blank=True, then required is set to False on the form field. Otherwise, required=True
Says so here: http://docs.djangoproject.com/en/dev/topics/forms/modelforms/
Looks like you are doing everything right.
You could check the value of self.fields['end'].required.
Expanding on DataGreed's answer, I created a Mixin that allows you to specify a fields_required variable on the Meta class like this:
class MyForm(RequiredFieldsMixin, ModelForm):
class Meta:
model = MyModel
fields = ['field1', 'field2']
fields_required = ['field1']
Here it is:
class RequiredFieldsMixin():
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
fields_required = getattr(self.Meta, 'fields_required', None)
if fields_required:
for key in self.fields:
if key not in fields_required:
self.fields[key].required = False
It's not an answer, but for anyone else who finds this via Google, one more bit of data: this is happening to me on a Model Form with a DateField. It has required set to False, the model has "null=True, blank=True" and the field in the form shows required=False if I look at it during the clean() method, but it's still saying I need a valid date format. I'm not using any special widget and I get the "Enter a valid date" message even when I explicitly set input_formats=['%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', ''] on the form field.
EDIT: Don't know if it'll help anyone else, but I solved the problem I was having. Our form has some default text in the field (in this case, the word "to" to indicate the field is the end date; the field is called "end_time"). I was specifically looking for the word "to" in the form's clean() method (I'd also tried the clean_end_time() method, but it never got called) and setting the value of the clean_data variable to None as suggested in this Django ticket. However, none of that mattered as (I guess) the model's validation had already puked on the invalid date format of "to" without giving me a chance to intercept it.
This is a bug when using the widgets:
workaround:
Using Django time/date widgets in custom form
or ticket 12303
From the model field documentation,
If you have a model as shown below,
class Article(models.Model):
headline = models.CharField(
max_length=200,
null=True,
blank=True,
help_text='Use puns liberally',
)
content = models.TextField()
You can change the headline's form validation to required=True instead of blank=False as that of the model as defining the field as shown below.
class ArticleForm(ModelForm):
headline = MyFormField(
max_length=200,
required=False,
help_text='Use puns liberally',
)
class Meta:
model = Article
fields = ['headline', 'content']
So answering the question,
class CircuitForm(ModelForm):
begin = forms.DateTimeField(required=False)
end = forms.DateTimeField(required=False)
class Meta:
model = Circuit
exclude = ('lastPaged',)
this makes begin and end to required=False

Categories

Resources