/django/contrib/admin/util.py
Python | 387 lines | 377 code | 2 blank | 8 comment | 1 complexity | 001d2f6feb873e6cc09c50ea85f413e0 MD5 | raw file
Possible License(s): BSD-3-Clause
1from django.db import models 2from django.db.models.sql.constants import LOOKUP_SEP 3from django.db.models.deletion import Collector 4from django.db.models.related import RelatedObject 5from django.forms.forms import pretty_name 6from django.utils import formats 7from django.utils.html import escape 8from django.utils.safestring import mark_safe 9from django.utils.text import capfirst 10from django.utils.encoding import force_unicode, smart_unicode, smart_str 11from django.utils.translation import ungettext 12from django.core.urlresolvers import reverse 13 14 15def quote(s): 16 """ 17 Ensure that primary key values do not confuse the admin URLs by escaping 18 any '/', '_' and ':' characters. Similar to urllib.quote, except that the 19 quoting is slightly different so that it doesn't get automatically 20 unquoted by the Web browser. 21 """ 22 if not isinstance(s, basestring): 23 return s 24 res = list(s) 25 for i in range(len(res)): 26 c = res[i] 27 if c in """:/_#?;@&=+$,"<>%\\""": 28 res[i] = '_%02X' % ord(c) 29 return ''.join(res) 30 31 32def unquote(s): 33 """ 34 Undo the effects of quote(). Based heavily on urllib.unquote(). 35 """ 36 mychr = chr 37 myatoi = int 38 list = s.split('_') 39 res = [list[0]] 40 myappend = res.append 41 del list[0] 42 for item in list: 43 if item[1:2]: 44 try: 45 myappend(mychr(myatoi(item[:2], 16)) + item[2:]) 46 except ValueError: 47 myappend('_' + item) 48 else: 49 myappend('_' + item) 50 return "".join(res) 51 52 53def flatten_fieldsets(fieldsets): 54 """Returns a list of field names from an admin fieldsets structure.""" 55 field_names = [] 56 for name, opts in fieldsets: 57 for field in opts['fields']: 58 # type checking feels dirty, but it seems like the best way here 59 if type(field) == tuple: 60 field_names.extend(field) 61 else: 62 field_names.append(field) 63 return field_names 64 65 66def get_deleted_objects(objs, opts, user, admin_site, using): 67 """ 68 Find all objects related to ``objs`` that should also be deleted. ``objs`` 69 must be a homogenous iterable of objects (e.g. a QuerySet). 70 71 Returns a nested list of strings suitable for display in the 72 template with the ``unordered_list`` filter. 73 74 """ 75 collector = NestedObjects(using=using) 76 collector.collect(objs) 77 perms_needed = set() 78 79 def format_callback(obj): 80 has_admin = obj.__class__ in admin_site._registry 81 opts = obj._meta 82 83 if has_admin: 84 admin_url = reverse('%s:%s_%s_change' 85 % (admin_site.name, 86 opts.app_label, 87 opts.object_name.lower()), 88 None, (quote(obj._get_pk_val()),)) 89 p = '%s.%s' % (opts.app_label, 90 opts.get_delete_permission()) 91 if not user.has_perm(p): 92 perms_needed.add(opts.verbose_name) 93 # Display a link to the admin page. 94 return mark_safe(u'%s: <a href="%s">%s</a>' % 95 (escape(capfirst(opts.verbose_name)), 96 admin_url, 97 escape(obj))) 98 else: 99 # Don't display link to edit, because it either has no 100 # admin or is edited inline. 101 return u'%s: %s' % (capfirst(opts.verbose_name), 102 force_unicode(obj)) 103 104 to_delete = collector.nested(format_callback) 105 106 protected = [format_callback(obj) for obj in collector.protected] 107 108 return to_delete, perms_needed, protected 109 110 111class NestedObjects(Collector): 112 def __init__(self, *args, **kwargs): 113 super(NestedObjects, self).__init__(*args, **kwargs) 114 self.edges = {} # {from_instance: [to_instances]} 115 self.protected = set() 116 117 def add_edge(self, source, target): 118 self.edges.setdefault(source, []).append(target) 119 120 def collect(self, objs, source_attr=None, **kwargs): 121 for obj in objs: 122 if source_attr: 123 self.add_edge(getattr(obj, source_attr), obj) 124 else: 125 self.add_edge(None, obj) 126 try: 127 return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs) 128 except models.ProtectedError, e: 129 self.protected.update(e.protected_objects) 130 131 def related_objects(self, related, objs): 132 qs = super(NestedObjects, self).related_objects(related, objs) 133 return qs.select_related(related.field.name) 134 135 def _nested(self, obj, seen, format_callback): 136 if obj in seen: 137 return [] 138 seen.add(obj) 139 children = [] 140 for child in self.edges.get(obj, ()): 141 children.extend(self._nested(child, seen, format_callback)) 142 if format_callback: 143 ret = [format_callback(obj)] 144 else: 145 ret = [obj] 146 if children: 147 ret.append(children) 148 return ret 149 150 def nested(self, format_callback=None): 151 """ 152 Return the graph as a nested list. 153 154 """ 155 seen = set() 156 roots = [] 157 for root in self.edges.get(None, ()): 158 roots.extend(self._nested(root, seen, format_callback)) 159 return roots 160 161 162def model_format_dict(obj): 163 """ 164 Return a `dict` with keys 'verbose_name' and 'verbose_name_plural', 165 typically for use with string formatting. 166 167 `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance. 168 169 """ 170 if isinstance(obj, (models.Model, models.base.ModelBase)): 171 opts = obj._meta 172 elif isinstance(obj, models.query.QuerySet): 173 opts = obj.model._meta 174 else: 175 opts = obj 176 return { 177 'verbose_name': force_unicode(opts.verbose_name), 178 'verbose_name_plural': force_unicode(opts.verbose_name_plural) 179 } 180 181 182def model_ngettext(obj, n=None): 183 """ 184 Return the appropriate `verbose_name` or `verbose_name_plural` value for 185 `obj` depending on the count `n`. 186 187 `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance. 188 If `obj` is a `QuerySet` instance, `n` is optional and the length of the 189 `QuerySet` is used. 190 191 """ 192 if isinstance(obj, models.query.QuerySet): 193 if n is None: 194 n = obj.count() 195 obj = obj.model 196 d = model_format_dict(obj) 197 singular, plural = d["verbose_name"], d["verbose_name_plural"] 198 return ungettext(singular, plural, n or 0) 199 200 201def lookup_field(name, obj, model_admin=None): 202 opts = obj._meta 203 try: 204 f = opts.get_field(name) 205 except models.FieldDoesNotExist: 206 # For non-field values, the value is either a method, property or 207 # returned via a callable. 208 if callable(name): 209 attr = name 210 value = attr(obj) 211 elif (model_admin is not None and hasattr(model_admin, name) and 212 not name == '__str__' and not name == '__unicode__'): 213 attr = getattr(model_admin, name) 214 value = attr(obj) 215 else: 216 attr = getattr(obj, name) 217 if callable(attr): 218 value = attr() 219 else: 220 value = attr 221 f = None 222 else: 223 attr = None 224 value = getattr(obj, name) 225 return f, attr, value 226 227 228def label_for_field(name, model, model_admin=None, return_attr=False): 229 attr = None 230 try: 231 field = model._meta.get_field_by_name(name)[0] 232 if isinstance(field, RelatedObject): 233 label = field.opts.verbose_name 234 else: 235 label = field.verbose_name 236 except models.FieldDoesNotExist: 237 if name == "__unicode__": 238 label = force_unicode(model._meta.verbose_name) 239 elif name == "__str__": 240 label = smart_str(model._meta.verbose_name) 241 else: 242 if callable(name): 243 attr = name 244 elif model_admin is not None and hasattr(model_admin, name): 245 attr = getattr(model_admin, name) 246 elif hasattr(model, name): 247 attr = getattr(model, name) 248 else: 249 message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name) 250 if model_admin: 251 message += " or %s" % (model_admin.__class__.__name__,) 252 raise AttributeError(message) 253 254 if hasattr(attr, "short_description"): 255 label = attr.short_description 256 elif callable(attr): 257 if attr.__name__ == "<lambda>": 258 label = "--" 259 else: 260 label = pretty_name(attr.__name__) 261 else: 262 label = pretty_name(name) 263 if return_attr: 264 return (label, attr) 265 else: 266 return label 267 268def help_text_for_field(name, model): 269 try: 270 help_text = model._meta.get_field_by_name(name)[0].help_text 271 except models.FieldDoesNotExist: 272 help_text = "" 273 return smart_unicode(help_text) 274 275 276def display_for_field(value, field): 277 from django.contrib.admin.templatetags.admin_list import _boolean_icon 278 from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE 279 280 if field.flatchoices: 281 return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE) 282 # NullBooleanField needs special-case null-handling, so it comes 283 # before the general null test. 284 elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField): 285 return _boolean_icon(value) 286 elif value is None: 287 return EMPTY_CHANGELIST_VALUE 288 elif isinstance(field, models.DateField) or isinstance(field, models.TimeField): 289 return formats.localize(value) 290 elif isinstance(field, models.DecimalField): 291 return formats.number_format(value, field.decimal_places) 292 elif isinstance(field, models.FloatField): 293 return formats.number_format(value) 294 else: 295 return smart_unicode(value) 296 297 298class NotRelationField(Exception): 299 pass 300 301 302def get_model_from_relation(field): 303 if isinstance(field, models.related.RelatedObject): 304 return field.model 305 elif getattr(field, 'rel'): # or isinstance? 306 return field.rel.to 307 else: 308 raise NotRelationField 309 310 311def reverse_field_path(model, path): 312 """ Create a reversed field path. 313 314 E.g. Given (Order, "user__groups"), 315 return (Group, "user__order"). 316 317 Final field must be a related model, not a data field. 318 319 """ 320 reversed_path = [] 321 parent = model 322 pieces = path.split(LOOKUP_SEP) 323 for piece in pieces: 324 field, model, direct, m2m = parent._meta.get_field_by_name(piece) 325 # skip trailing data field if extant: 326 if len(reversed_path) == len(pieces)-1: # final iteration 327 try: 328 get_model_from_relation(field) 329 except NotRelationField: 330 break 331 if direct: 332 related_name = field.related_query_name() 333 parent = field.rel.to 334 else: 335 related_name = field.field.name 336 parent = field.model 337 reversed_path.insert(0, related_name) 338 return (parent, LOOKUP_SEP.join(reversed_path)) 339 340 341def get_fields_from_path(model, path): 342 """ Return list of Fields given path relative to model. 343 344 e.g. (ModelX, "user__groups__name") -> [ 345 <django.db.models.fields.related.ForeignKey object at 0x...>, 346 <django.db.models.fields.related.ManyToManyField object at 0x...>, 347 <django.db.models.fields.CharField object at 0x...>, 348 ] 349 """ 350 pieces = path.split(LOOKUP_SEP) 351 fields = [] 352 for piece in pieces: 353 if fields: 354 parent = get_model_from_relation(fields[-1]) 355 else: 356 parent = model 357 fields.append(parent._meta.get_field_by_name(piece)[0]) 358 return fields 359 360 361def remove_trailing_data_field(fields): 362 """ Discard trailing non-relation field if extant. """ 363 try: 364 get_model_from_relation(fields[-1]) 365 except NotRelationField: 366 fields = fields[:-1] 367 return fields 368 369 370def get_limit_choices_to_from_path(model, path): 371 """ Return Q object for limiting choices if applicable. 372 373 If final model in path is linked via a ForeignKey or ManyToManyField which 374 has a `limit_choices_to` attribute, return it as a Q object. 375 """ 376 377 fields = get_fields_from_path(model, path) 378 fields = remove_trailing_data_field(fields) 379 limit_choices_to = ( 380 fields and hasattr(fields[-1], 'rel') and 381 getattr(fields[-1].rel, 'limit_choices_to', None)) 382 if not limit_choices_to: 383 return models.Q() # empty Q 384 elif isinstance(limit_choices_to, models.Q): 385 return limit_choices_to # already a Q 386 else: 387 return models.Q(**limit_choices_to) # convert dict to Q