I'm have a model that has some fields that should never be edited, such as "date created." I want to display a form that has some text fields with the extra information, probably just the data itself in a <span>. A person can edit a few fields and at the same time they can see the extra information about the instance of the model, such as created, last modified, or even related objects in the database for this object. Is there a way to do this using Django's built in framework, or will I have to create my own custom form by passing the whole object to a template?
If this needs more clarification, feel free to ask.
The best way I know to do this is to initialize the fields before you pass the form to the template by passing an initial dictionary to the form or by passing a instance object to the form.
You should then make sure that the fields are disabled, or you should make them hidden fields and then display the fields as regular text.
Most importantly, if you're passing data to the client that will then be sent back in a form, you should make sure that the data coming in is the same as the data that went out (for security's sake). Do this with at clean_[field] function on the Form. It should look like the following.
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def clean_date_created(self):
if self.cleaned_fields['date_created'] != self.instance.date_created:
raise ValidationError, 'date_created has been tampered'
self.cleaned_fields['date_created']
[Edit/Addendum] Alternatively, you can pass the data directly to your template to render separately, and then tack on the data to your form after you get it back into your view. It should go something like this:
def recieve_form(request, ...):
...
f = MyForm(request.POST, instance=a)
new_model_instance = f.save(commit=False)
new_model_instance.date_created = <whatever>
new_model_instance.save()
To do that I usually customize the form in order for the widget to be read only, like the following:
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
widgets = {
'my_field': forms.TextInput(attrs={'disabled':'disabled'}),
}
This will output a read only text field but I guess you can also create a Widget that will just output a <div> or a <p> (or whatever you need)
Related
Help needed with pre-populating Django forms: I have a form that updates a UserProfile model, when the form is loaded I want it pre-populated with the existing UserProfile data. Simple enough, the instance can be passed to the form. However, it seems that fields with choices (which come out as select drop-down elements in HTML) are not pre-populated with the instance data and instead default to '-----------'.
I've tried manually setting the initial value for a specific form (e.g. country) but it doesn't pull through to the HTML.
form = UserProfileForm(instance=user_profile)
form.fields['country'].initial = 'GBR'
I'm sure I could create a convoluted work around to get the current country selected in the front-end but it feels like it should be possible in Django. I couldn't see any solutions in other questions.
You can dynamically set default for a form field as:
form = UserProfileForm(instance=user_profile, initial={'country': 'GBR'})
If country field should be populated from user_profile data, then you can define form as following (assuming country is a field in UserProfile class)
class UserProfileForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(UserProfileForm, self).__init__(*args, **kwargs)
if self.instance:
self.initial['country'] = instance.country
Why what you are doing isn't working?
You are using:
form.fields['country'].initial = 'GBR'
This will not work on for a bounded form. form = UserProfileForm(instance=user_profile) will return a bounded form.
From docs:
The initial argument lets you specify the initial value to use when rendering this Field in an unbound Form.
To specify dynamic initial data, see the Form.initial parameter.
I'm trying to set up a django form consisting solely of a formset. In forms.py I have:
class StudentEnrolmentForm(forms.ModelForm):
school_class = forms.ModelChoiceField(SchoolClass.objects.currently_enrolling())
class Meta:
model = StudentApplication
fields = []
StudentEnrolmentFormSet = modelformset_factory(StudentApplication, StudentEnrolmentForm, extra=0)
but I'm unclear how to incorporate the FormSet into a CBV (In this case I've chosen a FormView). In this case I'm basically displaying a table of students, and allowing the operator to assign each student to a class. I only want a single 'submit' button at the end of the page.
If you will take a look on sources of Django views and check how FormView is working, you find that it just overrides default get and post methods of base View class and adds some additional methods for the form handling.
So you can:
try to assign your formset to the form_class field of your view and
play around. Probably you will have to override some additional
methods;
take a look on https://github.com/AndrewIngram/django-extra-views;
if options #1 and #2 causes too much pain - use default View
I need to generate several textareas to be filled by users and submitted back to the model. However, I think they need all to have different names (am i correct?)
How can I generate random names for each textarea and how can the model get the data within one it gets the POST request?
Thanks!
EDIT: I was hoping to use the randomly generated name as a way to identify the content and to save them in the database
It's hard to give you a good answer here because you haven't indicated what you're actually trying to achieve. You could add a 1000 text fields to your form, but if they don't correlate somehow to data on your model, there's not much point, and you've neglected that crucial piece of information.
Still, on a very basic level, you can add the additional textareas to your form like so:
class MyModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
for i in range(1, 11):
self.fields['mytextarea%d' % i] = forms.CharField(label='Textarea %d' % i, widget=forms.Textarea)
UPDATE
Based on your comment, and the fact that you're intending to actually store and retrieve the textarea fields at a later point, you should create a separate model for them and link it to your main model through a one-to-many relationship. For example:
class PrimaryModel(models.Model):
... # Stuff here
class TextareaModel(models.Model):
myprimarymodel = models.ForeignKey(PrimaryModel)
mytextarea = models.TextField()
It's hard to give a good example since you haven't indicated anything about what your models look like, but the basic idea is that you have a model that contains nothing but a foreign key to the main model and a textarea field. You can then treat these textareas as inlines with via a model formset.
I'm using SeparatedValuesField to keep track of a list of strings as explained by http://davidcramer.posterous.com/code/181/custom-fields-in-django.html and many posts here on SO that recommend this as the right way to store a list of strings.
class UserProfile(models.Model):
user = models.OneToOneField(User)
device_ids = SeparatedValuesField(blank=True, null=True, default=[])
It is working fine in my application, I can add device ids and view them in the admin interface as expected.
user_profile.device_ids = ['666666-D849-524F-6984-7E9B2D768546']
But the problem is in the admin interface, when I open up the detail page for a UserProfile object, the admin interface itself is adding extra values to my device_ids field.
For example, my application inserts a value into the field, and when I view it in the admin interface it looks like this:
[u'666666-D849-524F-6984-7E9B2D768546']
Then I change some other random property on my UserProfile object and save it using the built in django admin interface save button.
When I open the UserProfile object detail page up again for my object, that value now looks like this:
[u"[u'666666-D849-524F-6984-7E9B2D768546']"]
If I repeat this process of just hitting save then opening up this detail page, it will continue nesting the actual value with u"[ ] characters.
Is there something I can do to change this functionality? Should I be storing the list of strings in a different way?
I had the same exact problem, and finally figured it out thanks to these two different answers.
What you need in this situation is a custom widget that renders the list of strings as a string. This widget needs to be tied to the custom field:
class FlattenListWidget(forms.TextInput):
def render(self, name, value, attrs=None):
if not value is None:
value = ','.join(str(v) for v in value)
return super(FlattenListWidget, self).render(name, value, attrs)
class UserProfileAdminForm(forms.ModelForm):
class Meta:
model = UserModel
widgets = {
'device_ids': FlattenListWidget(),
}
class UserProfileAdmin(admin.ModelAdmin):
form = UserProfileAdminForm
Hope that works. I had different model names in my code but managed to get a text input containing a comma separated list of values from my custom field.
If you want to join the strings with a comma and a space value = ', '.join(str(v) for v in value), remember to strip() the value in the custom field's get_db_prep_value method. Otherwise the spaces will be saved back to database.
To use the custom field in list_display you can add a custom field to the admin model:
class UserProfileAdmin(admin.ModelAdmin):
form = UserProfileAdminForm
list_display = ('ids_list',)
def ids_list(self, obj):
if obj.device_ids:
return ', '.join(str(i) for i in obj.device_ids)
return None
Oiva Eskola's answer looks fine (haven't tried it, though).
Finding the Django source before this question, I also stumbled upon the following (which works):
SeparatedValuesField(models.TextField):
# ...
def value_from_object(self, obj):
return self.get_db_prep_value(super(SeparatedValuesField, self).value_from_object(obj))
I've been working through an ordered ManyToManyField widget, and have the front-end aspect of it working nicely:
Unfortunately, I'm having a great deal of trouble getting the backend working. The obvious way to hook up the backend is to use a through table keyed off a model with ForeignKeys to both sides of the relationship and overwrite the save method. This would work great, except that due to idiosyncrasies of the content, it is an absolute requirement that this widget be placed in a fieldset (using the ModelAdmin fieldsets property), which is apparently not possible.
I'm out of ideas. Any suggestions?
Thanks!
In regard to how to set up the models, you're right in that a through table with an "order" column is the ideal way to represent it. You're also right in that Django will not let you refer to that relationship in a fieldset. The trick to cracking this problem is to remember that the field names you specify in the "fieldsets" or "fields" of a ModelAdmin do not actually refer to the fields of the Model, but to the fields of the ModelForm, which we are free to override to our heart's delight. With many2many fields, this gets tricky, but bear with me:
Let's say you're trying to represent contests and competitors that compete in them, with an ordered many2many between contests and competitors where the order represents the competitors' ranking in that contest. Your models.py would then look like this:
from django.db import models
class Contest(models.Model):
name = models.CharField(max_length=50)
# More fields here, if you like.
contestants = models.ManyToManyField('Contestant', through='ContestResults')
class Contestant(models.Model):
name = models.CharField(max_length=50)
class ContestResults(models.Model):
contest = models.ForeignKey(Contest)
contestant = models.ForeignKey(Contestant)
rank = models.IntegerField()
Hopefully, this is similar to what you're dealing with. Now, for the admin. I've written an example admin.py with plenty of comments to explain what's happening, but here's a summary to help you along:
Since I don't have the code to the ordered m2m widget you've written, I've used a placeholder dummy widget that simply inherits from TextInput. The input holds a comma-separated list (without spaces) of contestant IDs, and the order of their appearance in the string determines the value of their "rank" column in the ContestResults model.
What happens is that we override the default ModelForm for Contest with our own, and then define a "results" field inside it (we can't call the field "contestants", since there would be a name conflict with the m2m field in the model). We then override __init__(), which is called when the form is displayed in the admin, so we can fetch any ContestResults that may have already been defined for the Contest, and use them to populate the widget. We also override save(), so that we can in turn get the data from the widget and create the needed ContestResults.
Note that for the sake of simplicity this example omits things like validation of the data from the widget, so things will break if you try to type in anything unexpected in the text input. Also, the code for creating the ContestResults is quite simplistic, and could be greatly improved upon.
I should also add that I've actually ran this code and verified that it works.
from django import forms
from django.contrib import admin
from models import Contest, Contestant, ContestResults
# Generates a function that sequentially calls the two functions that were
# passed to it
def func_concat(old_func, new_func):
def function():
old_func()
new_func()
return function
# A dummy widget to be replaced with your own.
class OrderedManyToManyWidget(forms.widgets.TextInput):
pass
# A simple CharField that shows a comma-separated list of contestant IDs.
class ResultsField(forms.CharField):
widget = OrderedManyToManyWidget()
class ContestAdminForm(forms.models.ModelForm):
# Any fields declared here can be referred to in the "fieldsets" or
# "fields" of the ModelAdmin. It is crucial that our custom field does not
# use the same name as the m2m field field in the model ("contestants" in
# our example).
results = ResultsField()
# Be sure to specify your model here.
class Meta:
model = Contest
# Override init so we can populate the form field with the existing data.
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance', None)
# See if we are editing an existing Contest. If not, there is nothing
# to be done.
if instance and instance.pk:
# Get a list of all the IDs of the contestants already specified
# for this contest.
contestants = ContestResults.objects.filter(contest=instance).order_by('rank').values_list('contestant_id', flat=True)
# Make them into a comma-separated string, and put them in our
# custom field.
self.base_fields['results'].initial = ','.join(map(str, contestants))
# Depending on how you've written your widget, you can pass things
# like a list of available contestants to it here, if necessary.
super(ContestAdminForm, self).__init__(*args, **kwargs)
def save(self, *args, **kwargs):
# This "commit" business complicates things somewhat. When true, it
# means that the model instance will actually be saved and all is
# good. When false, save() returns an unsaved instance of the model.
# When save() calls are made by the Django admin, commit is pretty
# much invariably false, though I'm not sure why. This is a problem
# because when creating a new Contest instance, it needs to have been
# saved in the DB and have a PK, before we can create ContestResults.
# Fortunately, all models have a built-in method called save_m2m()
# which will always be executed after save(), and we can append our
# ContestResults-creating code to the existing same_m2m() method.
commit = kwargs.get('commit', True)
# Save the Contest and get an instance of the saved model
instance = super(ContestAdminForm, self).save(*args, **kwargs)
# This is known as a lexical closure, which means that if we store
# this function and execute it later on, it will execute in the same
# context (i.e. it will have access to the current instance and self).
def save_m2m():
# This is really naive code and should be improved upon,
# especially in terms of validation, but the basic gist is to make
# the needed ContestResults. For now, we'll just delete any
# existing ContestResults for this Contest and create them anew.
ContestResults.objects.filter(contest=instance).delete()
# Make a list of (rank, contestant ID) tuples from the comma-
# -separated list of contestant IDs we get from the results field.
formdata = enumerate(map(int, self.cleaned_data['results'].split(',')), 1)
for rank, contestant in formdata:
ContestResults.objects.create(contest=instance, contestant_id=contestant, rank=rank)
if commit:
# If we're committing (fat chance), simply run the closure.
save_m2m()
else:
# Using a function concatenator, ensure our save_m2m closure is
# called after the existing save_m2m function (which will be
# called later on if commit is False).
self.save_m2m = func_concat(self.save_m2m, save_m2m)
# Return the instance like a good save() method.
return instance
class ContestAdmin(admin.ModelAdmin):
# The precious fieldsets.
fieldsets = (
('Basic Info', {
'fields': ('name', 'results',)
}),)
# Here's where we override our form
form = ContestAdminForm
admin.site.register(Contest, ContestAdmin)
In case you're wondering, I had ran into this problem myself on a project I've been working on, so most of this code comes from that project. I hope you find it useful.