/django/contrib/admin/widgets.py
Python | 296 lines | 266 code | 11 blank | 19 comment | 13 complexity | ae5532a23cfe1289edc1e2eff42eea85 MD5 | raw file
1""" 2Form Widget classes specific to the Django admin site. 3""" 4 5import django.utils.copycompat as copy 6 7from django import forms 8from django.forms.widgets import RadioFieldRenderer 9from django.forms.util import flatatt 10from django.utils.html import escape 11from django.utils.text import truncate_words 12from django.utils.translation import ugettext as _ 13from django.utils.safestring import mark_safe 14from django.utils.encoding import force_unicode 15from django.conf import settings 16from django.core.urlresolvers import reverse, NoReverseMatch 17 18class FilteredSelectMultiple(forms.SelectMultiple): 19 """ 20 A SelectMultiple with a JavaScript filter interface. 21 22 Note that the resulting JavaScript assumes that the jsi18n 23 catalog has been loaded in the page 24 """ 25 class Media: 26 js = (settings.ADMIN_MEDIA_PREFIX + "js/core.js", 27 settings.ADMIN_MEDIA_PREFIX + "js/SelectBox.js", 28 settings.ADMIN_MEDIA_PREFIX + "js/SelectFilter2.js") 29 30 def __init__(self, verbose_name, is_stacked, attrs=None, choices=()): 31 self.verbose_name = verbose_name 32 self.is_stacked = is_stacked 33 super(FilteredSelectMultiple, self).__init__(attrs, choices) 34 35 def render(self, name, value, attrs=None, choices=()): 36 if attrs is None: attrs = {} 37 attrs['class'] = 'selectfilter' 38 if self.is_stacked: attrs['class'] += 'stacked' 39 output = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)] 40 output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {') 41 # TODO: "id_" is hard-coded here. This should instead use the correct 42 # API to determine the ID dynamically. 43 output.append(u'SelectFilter.init("id_%s", "%s", %s, "%s"); });</script>\n' % \ 44 (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), settings.ADMIN_MEDIA_PREFIX)) 45 return mark_safe(u''.join(output)) 46 47class AdminDateWidget(forms.DateInput): 48 class Media: 49 js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", 50 settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js") 51 52 def __init__(self, attrs={}, format=None): 53 super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'}, format=format) 54 55class AdminTimeWidget(forms.TimeInput): 56 class Media: 57 js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", 58 settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js") 59 60 def __init__(self, attrs={}, format=None): 61 super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'}, format=format) 62 63class AdminSplitDateTime(forms.SplitDateTimeWidget): 64 """ 65 A SplitDateTime Widget that has some admin-specific styling. 66 """ 67 def __init__(self, attrs=None): 68 widgets = [AdminDateWidget, AdminTimeWidget] 69 # Note that we're calling MultiWidget, not SplitDateTimeWidget, because 70 # we want to define widgets. 71 forms.MultiWidget.__init__(self, widgets, attrs) 72 73 def format_output(self, rendered_widgets): 74 return mark_safe(u'<p class="datetime">%s %s<br />%s %s</p>' % \ 75 (_('Date:'), rendered_widgets[0], _('Time:'), rendered_widgets[1])) 76 77class AdminRadioFieldRenderer(RadioFieldRenderer): 78 def render(self): 79 """Outputs a <ul> for this set of radio fields.""" 80 return mark_safe(u'<ul%s>\n%s\n</ul>' % ( 81 flatatt(self.attrs), 82 u'\n'.join([u'<li>%s</li>' % force_unicode(w) for w in self])) 83 ) 84 85class AdminRadioSelect(forms.RadioSelect): 86 renderer = AdminRadioFieldRenderer 87 88class AdminFileWidget(forms.ClearableFileInput): 89 template_with_initial = (u'<p class="file-upload">%s</p>' 90 % forms.ClearableFileInput.template_with_initial) 91 template_with_clear = (u'<span class="clearable-file-input">%s</span>' 92 % forms.ClearableFileInput.template_with_clear) 93 94def url_params_from_lookup_dict(lookups): 95 """ 96 Converts the type of lookups specified in a ForeignKey limit_choices_to 97 attribute to a dictionary of query parameters 98 """ 99 params = {} 100 if lookups and hasattr(lookups, 'items'): 101 items = [] 102 for k, v in lookups.items(): 103 if isinstance(v, list): 104 v = u','.join([str(x) for x in v]) 105 elif isinstance(v, bool): 106 # See django.db.fields.BooleanField.get_prep_lookup 107 v = ('0', '1')[v] 108 else: 109 v = unicode(v) 110 items.append((k, v)) 111 params.update(dict(items)) 112 return params 113 114class ForeignKeyRawIdWidget(forms.TextInput): 115 """ 116 A Widget for displaying ForeignKeys in the "raw_id" interface rather than 117 in a <select> box. 118 """ 119 def __init__(self, rel, attrs=None, using=None): 120 self.rel = rel 121 self.db = using 122 super(ForeignKeyRawIdWidget, self).__init__(attrs) 123 124 def render(self, name, value, attrs=None): 125 if attrs is None: 126 attrs = {} 127 related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower()) 128 params = self.url_parameters() 129 if params: 130 url = u'?' + u'&'.join([u'%s=%s' % (k, v) for k, v in params.items()]) 131 else: 132 url = u'' 133 if "class" not in attrs: 134 attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript looks for this hook. 135 output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)] 136 # TODO: "id_" is hard-coded here. This should instead use the correct 137 # API to determine the ID dynamically. 138 output.append(u'<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' % \ 139 (related_url, url, name)) 140 output.append(u'<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup'))) 141 if value: 142 output.append(self.label_for_value(value)) 143 return mark_safe(u''.join(output)) 144 145 def base_url_parameters(self): 146 return url_params_from_lookup_dict(self.rel.limit_choices_to) 147 148 def url_parameters(self): 149 from django.contrib.admin.views.main import TO_FIELD_VAR 150 params = self.base_url_parameters() 151 params.update({TO_FIELD_VAR: self.rel.get_related_field().name}) 152 return params 153 154 def label_for_value(self, value): 155 key = self.rel.get_related_field().name 156 try: 157 obj = self.rel.to._default_manager.using(self.db).get(**{key: value}) 158 return ' <strong>%s</strong>' % escape(truncate_words(obj, 14)) 159 except (ValueError, self.rel.to.DoesNotExist): 160 return '' 161 162class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): 163 """ 164 A Widget for displaying ManyToMany ids in the "raw_id" interface rather than 165 in a <select multiple> box. 166 """ 167 def render(self, name, value, attrs=None): 168 if attrs is None: 169 attrs = {} 170 attrs['class'] = 'vManyToManyRawIdAdminField' 171 if value: 172 value = ','.join([force_unicode(v) for v in value]) 173 else: 174 value = '' 175 return super(ManyToManyRawIdWidget, self).render(name, value, attrs) 176 177 def url_parameters(self): 178 return self.base_url_parameters() 179 180 def label_for_value(self, value): 181 return '' 182 183 def value_from_datadict(self, data, files, name): 184 value = data.get(name) 185 if value: 186 return value.split(',') 187 188 def _has_changed(self, initial, data): 189 if initial is None: 190 initial = [] 191 if data is None: 192 data = [] 193 if len(initial) != len(data): 194 return True 195 for pk1, pk2 in zip(initial, data): 196 if force_unicode(pk1) != force_unicode(pk2): 197 return True 198 return False 199 200class RelatedFieldWidgetWrapper(forms.Widget): 201 """ 202 This class is a wrapper to a given widget to add the add icon for the 203 admin interface. 204 """ 205 def __init__(self, widget, rel, admin_site, can_add_related=None): 206 self.is_hidden = widget.is_hidden 207 self.needs_multipart_form = widget.needs_multipart_form 208 self.attrs = widget.attrs 209 self.choices = widget.choices 210 self.widget = widget 211 self.rel = rel 212 # Backwards compatible check for whether a user can add related 213 # objects. 214 if can_add_related is None: 215 can_add_related = rel.to in admin_site._registry 216 self.can_add_related = can_add_related 217 # so we can check if the related object is registered with this AdminSite 218 self.admin_site = admin_site 219 220 def __deepcopy__(self, memo): 221 obj = copy.copy(self) 222 obj.widget = copy.deepcopy(self.widget, memo) 223 obj.attrs = self.widget.attrs 224 memo[id(self)] = obj 225 return obj 226 227 def _media(self): 228 return self.widget.media 229 media = property(_media) 230 231 def render(self, name, value, *args, **kwargs): 232 rel_to = self.rel.to 233 info = (rel_to._meta.app_label, rel_to._meta.object_name.lower()) 234 try: 235 related_url = reverse('admin:%s_%s_add' % info, current_app=self.admin_site.name) 236 except NoReverseMatch: 237 info = (self.admin_site.root_path, rel_to._meta.app_label, rel_to._meta.object_name.lower()) 238 related_url = '%s%s/%s/add/' % info 239 self.widget.choices = self.choices 240 output = [self.widget.render(name, value, *args, **kwargs)] 241 if self.can_add_related: 242 # TODO: "id_" is hard-coded here. This should instead use the correct 243 # API to determine the ID dynamically. 244 output.append(u'<a href="%s" class="add-another" id="add_id_%s" onclick="return showAddAnotherPopup(this);"> ' % \ 245 (related_url, name)) 246 output.append(u'<img src="%simg/admin/icon_addlink.gif" width="10" height="10" alt="%s"/></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Add Another'))) 247 return mark_safe(u''.join(output)) 248 249 def build_attrs(self, extra_attrs=None, **kwargs): 250 "Helper function for building an attribute dictionary." 251 self.attrs = self.widget.build_attrs(extra_attrs=None, **kwargs) 252 return self.attrs 253 254 def value_from_datadict(self, data, files, name): 255 return self.widget.value_from_datadict(data, files, name) 256 257 def _has_changed(self, initial, data): 258 return self.widget._has_changed(initial, data) 259 260 def id_for_label(self, id_): 261 return self.widget.id_for_label(id_) 262 263class AdminTextareaWidget(forms.Textarea): 264 def __init__(self, attrs=None): 265 final_attrs = {'class': 'vLargeTextField'} 266 if attrs is not None: 267 final_attrs.update(attrs) 268 super(AdminTextareaWidget, self).__init__(attrs=final_attrs) 269 270class AdminTextInputWidget(forms.TextInput): 271 def __init__(self, attrs=None): 272 final_attrs = {'class': 'vTextField'} 273 if attrs is not None: 274 final_attrs.update(attrs) 275 super(AdminTextInputWidget, self).__init__(attrs=final_attrs) 276 277class AdminURLFieldWidget(forms.TextInput): 278 def __init__(self, attrs=None): 279 final_attrs = {'class': 'vURLField'} 280 if attrs is not None: 281 final_attrs.update(attrs) 282 super(AdminURLFieldWidget, self).__init__(attrs=final_attrs) 283 284class AdminIntegerFieldWidget(forms.TextInput): 285 def __init__(self, attrs=None): 286 final_attrs = {'class': 'vIntegerField'} 287 if attrs is not None: 288 final_attrs.update(attrs) 289 super(AdminIntegerFieldWidget, self).__init__(attrs=final_attrs) 290 291class AdminCommaSeparatedIntegerFieldWidget(forms.TextInput): 292 def __init__(self, attrs=None): 293 final_attrs = {'class': 'vCommaSeparatedIntegerField'} 294 if attrs is not None: 295 final_attrs.update(attrs) 296 super(AdminCommaSeparatedIntegerFieldWidget, self).__init__(attrs=final_attrs)