Form widget for text inputs with external link wanted! - django

I keep field imdb_id for models Movie in my db:
class Movie(models.Model):
imdb_id = models.IntegerField('imdb ID', blank=True, null=True, unique=True)
def _get_imdb_url(self):
return self.imdb_id and 'http://www.imdb.com/title/tt%s/' % str(self.imdb_id).zfill(7) or ''
def _set_imdb_url(self, imdb_url):
self.imdb_id = int( re.compile(r'[^\d]').sub('', imdb_url))
imdb_url = property(_get_imdb_url, _set_imdb_url)
And I want to make special widget for displaying external link to imdb.com in the admin form near text input for field 'imdb_id'. I think it may be global widget for any form field with external link, generated by using special mask (in my case, this mask is 'http://www.imdb.com/title/tt%s/'). I know how to write widget, but I don't know how push my mask, defined in my Movie model, to this widget. I don't want to violate DRY principe and define this mask in two different places. And also it will be a good tool for the same purpose with other external links in future.
What do you think about this widget? How it is posssible to realize it? May be someone wrote it before me?
Thanks!

So, I decided not waiting for help and wrote this widget:
import re
from django import forms
class LinkFieldWidget(Widget):
'''
A TextField widget with previewing link, generated from field's value with special url mask
'''
def __init__(self, text, url='%s', *args, **kwargs):
self.url = url
self.text = text
if not re.search('%', self.url):
raise forms.ValidationError, u'Invalid URL mask'
super(LinkFieldWidget, self).__init__(*args, **kwargs)
def render(self, name, value, attrs=None):
output = []
output.append('''<input type="text" name="%s" value="%s" onkeyup="$('a#%s-link').attr('href', '%s'.replace('%s', this.value)).css({'display': (this.value ? 'inline' : 'none')})" />''' % (name, value or '', name, self.url, '%s'))
if self.url and self.text:
output.append('<span>%s</span>' % (value and self.url % value or '#', name, value and 'inline' or 'none', self.text))
return mark_safe(u' '.join(output))
Example using it in admin:
from utils.widgets import LinkFieldWidget
class MovieAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MovieAdminForm, self).__init__(*args, **kwargs)
self.fields['imdb_id'].widget = LinkFieldWidget(text='imdb', url=self.instance.imdb_link)
class MovieAdmin(admin.ModelAdmin):
form = MovieAdminForm
Model:
class Movie(models.Model):
.....
imdb_link = 'http://www.imdb.com/title/tt%s/'
def _get_imdb_url(self):
return self.imdb_id and self.imdb_link % str(self.imdb_id).zfill(7) or ''
def _set_imdb_url(self, imdb_url):
self.imdb_id = int( re.compile(r'[^\d]').sub('', imdb_url))
imdb_url = property(_get_imdb_url, _set_imdb_url)

Related

Trying to build a custom Django file upload form field

I am trying to get a file upload form field working in Django and the part I am having problems with is dynamically changing the form field required attribute. I have tried using "self.fields['field_name'].required=True' in the init method of the form but that isn't working for me.
I have looked at Django dynamically changing the required property on forms but I don't want to build several custom models and a custom render function for one form as surely it must be easier than that.
The reason I am trying to do this is because when a django form validates and has errors it doesn't pass any uploaded files back to the browser form for reediting. It will pass text areas and text inputs that didn't validate back to the form for reediting but not file uploads. I thought if I made the file upload fields mandatory for the first time the record is created mandatory and for subsequent times make them optional. That is basically what I am trying to do.
So here is what I have been trying so far:
In forms.py
from django.forms import fields
from .widgets import PDFUploadWidget, PlainTextWidget
class WQPDFField(fields.Field):
widget = PDFUploadWidget
def widget_attrs(self, widget):
attrs = super().widget_attrs(widget)
attrs['label'] = self.label
return attrs
def clean(self, *args, **kwargs):
return super().clean(*args, **kwargs)
In widgets.py
from django.forms import widgets
from django.utils.safestring import mark_safe
# We subclass from HiddenInput because we want to suppress the printing
# of the label and prefer to print it ourselves.
class PDFUploadWidget(widgets.HiddenInput):
template_name = 'webquest_widgets/widgets/pdf_upload.html'
input_type = 'file'
def __init__(self, *args, **kwargs):
style = 'visibility:hidden'
attrs = kwargs.pop('attrs', None)
if attrs:
attrs['style'] = style
else:
attrs = {'style':style}
attrs['accept'] = '.pdf'
print (attrs)
super().__init__(attrs)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
return context
#property
def is_hidden(self):
return True
class Media:
css = { 'all': ( 'css/pdfupload.css', ) }
js = ('js/pdfupload.js', )
and finally in forms.py
class WorkWantedForm(forms.Form):
category = forms.ChoiceField(choices=CHOICES)
about = forms.CharField(label="About Yourself", widget=forms.Textarea())
static1 = WQStaticField(text="Enter either phone or email")
phone = forms.CharField(required=False)
email = forms.EmailField(required=False)
cv = WQPDFField(label="Upload CV")
supporting_document = WQPDFField(label="Supporting Document (optional)", required=False)
I am not sure how to pass the "required" attribute to the custom field class after the initialisation of the form but before rendering the form as HTML.

formset and input text for each foreign key

In my django formset, I am trying to display a foreign key field with an input instead of a select:
class MinAttend(models.Model):
act = models.ForeignKey(Act)
country = models.ForeignKey(Country)
verbatim = models.ForeignKey(Verbatim)
def __unicode__(self):
return u"%s" % self.verbatim
class MinAttendForm(forms.ModelForm):
country=forms.ModelChoiceField(queryset=Country.objects.all(), empty_label="Select a country")
status=forms.ModelChoiceField(queryset=Status.objects.values_list('status', flat = True).distinct(), empty_label="Select a status")
verbatim=forms.CharField(max_length=300)
class Meta:
model=MinAttend
#fields used for the validation and order
fields = ('country', 'status', 'verbatim')
For the verbatim field, I do have an input box instead of a select but when I want to update a formset, I have the verbatim id instead of its corresponding text:
Here is how I initialize the form:
class MinAttendUpdate(UpdateView):
object=None
model = MinAttend
form_class=MinAttendForm
nb_extra_forms=3
def post(self, request, *args, **kwargs):
attendances=MinAttend.objects.filter(...)
#set the number of forms to the number of ministers + 3 extra form to fill if needed
MinAttendFormSet = modelformset_factory(self.model, form=self.form_class, extra=len(attendances), max_num=len(attendances)+self.nb_extra_forms, can_delete=True)
formset=MinAttendFormSet(queryset=attendances)
I have tried two things:
Instead of the last line I have the following code:
initials=[]
#display text of verbatim instead of id
for index in range(len(attendances)):
initials.append({"verbatim": attendances[index].verbatim.verbatim})
print "initials", initials
formset=MinAttendFormSet(queryset=attendances, initial=initials)
I have overridden the init method of the form:
#~ #verbatim text instead of id for the verbatim field
def __init__(self, *args, **kwargs):
super(MinAttendForm, self).__init__(*args, **kwargs)
instance = kwargs.get("instance", None)
if instance!=None:
print "instance", instance.verbatim.verbatim
self.fields["verbatim"].initial = instance.verbatim.verbatim
None of these methods works, I still get numbers instead of text! What is curious is that I do see text for the verbatim field but only for the three extra forms. Normal?
EDIT - from Bernd Jerzyna comment
In my form:
from django.forms.models import BaseModelFormSet
class MinAttendFormSet(BaseModelFormSet):
def __init__(self, *args, **kwargs):
super(MinAttendFormSet, self).__init__(*args, **kwargs)
for form in self.forms:
#skip extra forms
if not form.empty_permitted:
form.fields['verbatim'].initial= form.instance.verbatim
print "verbatim MinAttendFormSet", form.instance.verbatim
In my view:
from forms import MinAttendForm, MinAttendFormSet
my_formset = modelformset_factory(self.model, form=self.form_class, formset=MinAttendFormSet)
formset = my_formset(request.POST, queryset=attendances)
When I do a print of the text of each verbatim, I see the correct text displayed. However, I still see numbers (primary key ids) in the form of my web page ;(.
What's wrong?
I think something like this should work:
Implement the init method of your formset like
def __init__(self, *args, **kwargs):
...
super(MinAttendFormSet, self).__init__(*args, **kwargs)
for form in self.forms:
form.fields['verbatim'].initial= form.instance.verbatim
That would also require your Verbatim model to have something like:
def __unicode__(self):
return u'%s' % (self.name)
This is my understanding of it...
As a select widget, the verbatim is displayed with the __unicode__ results, but the value 'behind the scenes' is the PK/verbatim-id, ie the HTML <option> tag has a value and 'label'. When you change it to a text input widget, it is expecting you to enter the PK, hence why you are seeing the numbers.
For the solution, I am not sure. You could write some code to accept the verbatim-text, rather than the pk/verbatim-id. The problem with this though, is that if the text is not written exactly, django won't find a match. Also, if more than 1 verbatim has the same text, django wouldn't know which one to use (unless you have set unique=true on the model field for text). You could also set the verbatim-text as the PK.
Perhaps using something like Django-Select2 will give you the desired UI?

Django AdminForm field default value

I have a Django admin form.
And now I want to fill it's initial field with data based on my model. So I tried this:
class OrderForm(forms.ModelForm):
class Meta:
model = Order
email = CharField(initial="null", widget=Textarea(attrs={'rows': 30, 'cols': 100}))
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
products = kwargs['instance'].products.all()
self.message = purchase_message % (
"".join(["<li>" + p.name + ": " + str(p.price) + "</li>" for p in products]),
reduce(lambda x, y:x + y.price, products, 0)
)
# and here I have a message in self.message variable
super(OrderForm, self).__init__(*args, **kwargs)
At this point i don't know how to access email field to set it's initial value before widget is rendered. How can i do this?
Assuming the value is based on 'request' you should use this:
class MyModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super(MyModelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['my_field_name'].initial = 'abcd'
return form
Since Django 1.7 there is a function get_changeform_initial_data in ModelAdmin that sets initial form values:
def get_changeform_initial_data(self, request):
return {'name': 'custom_initial_value'}
EDIT: Apart from that, #Paul Kenjora's answer applies anyway, which might be useful if you already override get_form.
In case of inline (InlineModelAdmin) there is no get_changeform_initial_data. You can override get_formset and set formset.form.base_fields['my_field_name'].initial.
I'm not too sure what you need to set email to, but You can set the initial values in lots of different places.
Your function def init() isn't indented correctly which i guess is a typo? Also, why are you specifically giving the email form field a TextInput? It already renders this widget by default
You can set the email's initial value in your form's initialized (def __ init __(self))
(self.fields['email'].widget).initial_value = "something"
or in the model.py
email = models.CharField(default="something")
or as you have in forms.py
email = models.CharField(initial="something")
I needed the first solution of pastylegs since the other ones overwrite the whole Widget including, for example, the help text. However, it didn't work for me as he posted it. Instead, I had to do this:
self.fields['email'].initial = 'something'
In my case, I was trying to do a personalized auto-increment(based on current data and not a simple default) in a field of a django admin form.
This code is worked for me (Django 1.11):
from django import forms
class MyAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.initial['field_name'] = 'initial_value'

Django forms: how to dynamically create ModelChoiceField labels

I would like to create dynamic labels for a forms.ModelChoiceField and I'm wondering how to do that. I have the following form class:
class ProfileForm(forms.ModelForm):
def __init__(self, data=None, ..., language_code='en', family_name_label='Family name', horoscope_label='Horoscope type', *args, **kwargs):
super(ProfileForm, self).__init__(data, *args, **kwargs)
self.fields['family_name'].label = family_name_label
.
.
self.fields['horoscope'].label = horoscope_label
self.fields['horoscope'].queryset = Horoscope.objects.all()
class Meta:
model = Profile
family_name = forms.CharField(widget=forms.TextInput(attrs={'size':'80', 'class': 'contact_form'}))
.
.
horoscope = forms.ModelChoiceField(queryset = Horoscope.objects.none(), widget=forms.RadioSelect(), empty_label=None)
The default labels are defined by the unicode function specified in the Profile definition. However the labels for the radio buttons created by the ModelChoiceField need to be created dynamically.
First I thought I could simply override ModelChoiceField as described in the Django documentation. But that creates static labels. It allows you to define any label but once the choice is made, that choice is fixed.
So I think I need to adapt add something to init like:
class ProfileForm(forms.ModelForm):
def __init__(self, data=None, ..., language_code='en', family_name_label='Family name', horoscope_label='Horoscope type', *args, **kwargs):
super(ProfileForm, self).__init__(data, *args, **kwargs)
self.fields['family_name'].label = family_name_label
.
.
self.fields['horoscope'].label = horoscope_label
self.fields['horoscope'].queryset = Horoscope.objects.all()
self.fields['horoscope'].<WHAT>??? = ???
Anyone having any idea how to handle this? Any help would be appreciated very much.
I found something but I don't know if it's the best solution. I add something to the init part of class ProfileForm as follows:
class ProfileForm((forms.ModelForm):
def __init__(self, data=None, ..., language_code='en', family_name_label='Family name', horoscope_label='Horoscope type', *args, **kwargs):
super(ProfileForm, self).__init__(data, *args, **kwargs)
# this function is added
def get_label(self, language_code):
"""
returns the label in the designated language, from a related object (table)
"""
return HoroscopeLanguage.objects.get(horoscope=obj, language__language_code=language_code).horoscope_type_language
self.fields['family_name'].label = family_name_label
.
.
self.fields['horoscope'].queryset = Horoscope.objects.all()
self.fields['horoscope'].label_from_instance = lambda obj: "%s: Euro %.2f" % (HoroscopeLanguage.objects.get(horoscope=obj, language__language_code=language_code).horoscope_type_language, obj.price)
.
.
"""
The next code also works, the lambda function without the get_label function
"""
self.fields['horoscope'].label_from_instance = lambda obj: "%s: Euro %.2f" % (obj.horoscope_type, obj.price)
.
.
"""
But this code doesn't work. Anyone?
"""
self.fields['horoscope'].label_from_instance = get_label(obj, language_code)
You could use a ModelChoiceField and then change the choices in you ProfileForm.__init__ dynamically, eg (assuming that it is already a ModelChoiceField):
horoscopes = Horoscope.objects.all()
self.fields['horoscope'].choices = [(h.pk, h.name) for h in horoscopes]
h.name in this example will be used as the label of the choice!
You can make your own form field class and overwrite the method that generates the label:
class MyChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
# return your own label here...
return smart_unicode(obj)
use it in your form same as you did with the ModelChoiceField:
horoscope = MyChoiceField(queryset = .....)
Actually the last code example contains errors an should be:
# this function is added
def get_label(obj):
return '%s: Euro %.2f' % (HoroscopeLanguage.objects.get(horoscope=obj, language__language_code=language_code).horoscope_type_language, obj.price)
.
.
.
self.fields['horoscope'].label_from_instance = get_labels
Then it works. There is no difference in using 'lambda obj:...' or 'def get_label(obj): ...'

forms.SelectMultiple from models.CommaSeparatedIntegerField

I have a model with field:
class Movie(models.Model):
genre = models.CommaSeparatedIntegerField(max_length=100, choices=GENRE_CHOICES, blank=True, default=0)
lang = models.CommaSeparatedIntegerField(max_length=100, choices=LANG_CHOICES, blank=True, default=0)
And I need to get multiple select fields (not checkboxes) from that.
One way, that i found, is to redefine form from ModelAdmin
class MyMovieAdminForm(forms.ModelForm):
genre = forms.MultipleChoiceField(choices=GENRE_CHOICES)
lang = forms.MultipleChoiceField(choices=LANG_CHOICES)
class MovieAdmin(admin.ModelAdmin):
form = MyMovieAdminForm
admin.site.register(Movie, MovieAdmin)
But it need to redeclare 'label' and 'initial' for each field, that isn't good for DRY principle. And I doesn't understand, how can I set current value of object for initial value of each field?
And other way, that I found in manual is formfield-overrides. I use dev version from trunk and I try to use this code, but it didn't change my select fields to multiselect in admin interface:
class MovieAdmin(admin.ModelAdmin):
formfield_overrides = {
models.CommaSeparatedIntegerField: {'widget': forms.SelectMultiple},
}
May be anyone know, what is the best way to define multiple select fields? Thanks!
I don't find any working answer for making models.CommaSeparatedIntegerField as forms.SelectMultiple. So I changed models.CommaSeparatedIntegerField to models.ManyToManyField and form field becomes works very well! It is more suitable in cases, where you need to make queries on this field.
1) Remove choices from models.CommaSeparatedIntegerField in models
2) Create CommaSeparatedCharField as replacement
class CommaSeparatedCharField(forms.CharField):
def to_python(self, value):
if value in validators.EMPTY_VALUES:
return u''
csv = smart_unicode(','.join(value) )
return csv
class CommaSeparatedSelectMultiple(forms.SelectMultiple):
def render(self, *args, **kwargs):
print args, kwargs
args_=list(args)
if type(args_[1])!=list:
args_[1] = args_[1].split(',')
return super(CommaSeparatedSelectMultiple, self).render(*args_, **kwargs)
3) Apply
class MyAdmin(admin.ModelAdmin):
class form(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(forms.ModelForm, self).__init__(*args, **kwargs)
for field_name, choices in [
('minutes', [(i,str(i)) for i in range(60)]),
]:
self.fields[field_name] = CommaSeparatedCharField(
label=self.fields[field_name].label,
initial=self.fields[field_name].initial,
widget=forms.CommaSeparatedSelectMultiple(choices=choices)
)
Work nice on 1.2 trunk
I use the following for somewhat-DRYer form manipulation, and it works with 1.0. It's verbose, but it works.
class MyMovieAdminForm(forms.ModelForm):
## Meta, etc
def __init__(self, *args, **kwargs):
super(MyMovieAdminForm, self).__init__(*args, **kwargs)
self.fields["genre"].widget = forms.SelectMultiple(choices=foo)
self.fields["genre"].initial = self.instance.genre
# Doesn't require redefining label, etc.