1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
|
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.forms.forms import pretty_name
from django.utils import formats
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.encoding import force_unicode, smart_unicode, smart_str
from django.utils.translation import ungettext, ugettext as _
from django.core.urlresolvers import reverse, NoReverseMatch
from django.utils.datastructures import SortedDict
def quote(s):
"""
Ensure that primary key values do not confuse the admin URLs by escaping
any '/', '_' and ':' characters. Similar to urllib.quote, except that the
quoting is slightly different so that it doesn't get automatically
unquoted by the Web browser.
"""
if not isinstance(s, basestring):
return s
res = list(s)
for i in range(len(res)):
c = res[i]
if c in """:/_#?;@&=+$,"<>%\\""":
res[i] = '_%02X' % ord(c)
return ''.join(res)
def unquote(s):
"""
Undo the effects of quote(). Based heavily on urllib.unquote().
"""
mychr = chr
myatoi = int
list = s.split('_')
res = [list[0]]
myappend = res.append
del list[0]
for item in list:
if item[1:2]:
try:
myappend(mychr(myatoi(item[:2], 16)) + item[2:])
except ValueError:
myappend('_' + item)
else:
myappend('_' + item)
return "".join(res)
def flatten_fieldsets(fieldsets):
"""Returns a list of field names from an admin fieldsets structure."""
field_names = []
for name, opts in fieldsets:
for field in opts['fields']:
# type checking feels dirty, but it seems like the best way here
if type(field) == tuple:
field_names.extend(field)
else:
field_names.append(field)
return field_names
def _format_callback(obj, user, admin_site, levels_to_root, perms_needed):
has_admin = obj.__class__ in admin_site._registry
opts = obj._meta
try:
admin_url = reverse('%s:%s_%s_change'
% (admin_site.name,
opts.app_label,
opts.object_name.lower()),
None, (quote(obj._get_pk_val()),))
except NoReverseMatch:
admin_url = '%s%s/%s/%s/' % ('../'*levels_to_root,
opts.app_label,
opts.object_name.lower(),
quote(obj._get_pk_val()))
if has_admin:
p = '%s.%s' % (opts.app_label,
opts.get_delete_permission())
if not user.has_perm(p):
perms_needed.add(opts.verbose_name)
# Display a link to the admin page.
return mark_safe(u'%s: <a href="%s">%s</a>' %
(escape(capfirst(opts.verbose_name)),
admin_url,
escape(obj)))
else:
# Don't display link to edit, because it either has no
# admin or is edited inline.
return u'%s: %s' % (capfirst(opts.verbose_name),
force_unicode(obj))
def get_deleted_objects(objs, opts, user, admin_site, levels_to_root=4):
"""
Find all objects related to ``objs`` that should also be
deleted. ``objs`` should be an iterable of objects.
Returns a nested list of strings suitable for display in the
template with the ``unordered_list`` filter.
`levels_to_root` defines the number of directories (../) to reach
the admin root path. In a change_view this is 4, in a change_list
view 2.
This is for backwards compatibility since the options.delete_selected
method uses this function also from a change_list view.
This will not be used if we can reverse the URL.
"""
collector = NestedObjects()
for obj in objs:
# TODO using a private model API!
obj._collect_sub_objects(collector)
perms_needed = set()
to_delete = collector.nested(_format_callback,
user=user,
admin_site=admin_site,
levels_to_root=levels_to_root,
perms_needed=perms_needed)
return to_delete, perms_needed
class NestedObjects(object):
"""
A directed acyclic graph collection that exposes the add() API
expected by Model._collect_sub_objects and can present its data as
a nested list of objects.
"""
def __init__(self):
# Use object keys of the form (model, pk) because actual model
# objects may not be unique
# maps object key to list of child keys
self.children = SortedDict()
# maps object key to parent key
self.parents = SortedDict()
# maps object key to actual object
self.seen = SortedDict()
def add(self, model, pk, obj,
parent_model=None, parent_obj=None, nullable=False):
"""
Add item ``obj`` to the graph. Returns True (and does nothing)
if the item has been seen already.
The ``parent_obj`` argument must already exist in the graph; if
not, it's ignored (but ``obj`` is still added with no
parent). In any case, Model._collect_sub_objects (for whom
this API exists) will never pass a parent that hasn't already
been added itself.
These restrictions in combination ensure the graph will remain
acyclic (but can have multiple roots).
``model``, ``pk``, and ``parent_model`` arguments are ignored
in favor of the appropriate lookups on ``obj`` and
``parent_obj``; unlike CollectedObjects, we can't maintain
independence from the knowledge that we're operating on model
instances, and we don't want to allow for inconsistency.
``nullable`` arg is ignored: it doesn't affect how the tree of
collected objects should be nested for display.
"""
model, pk = type(obj), obj._get_pk_val()
# auto-created M2M models don't interest us
if model._meta.auto_created:
return True
key = model, pk
if key in self.seen:
return True
self.seen.setdefault(key, obj)
if parent_obj is not None:
parent_model, parent_pk = (type(parent_obj),
parent_obj._get_pk_val())
parent_key = (parent_model, parent_pk)
if parent_key in self.seen:
self.children.setdefault(parent_key, list()).append(key)
self.parents.setdefault(key, parent_key)
def _nested(self, key, format_callback=None, **kwargs):
obj = self.seen[key]
if format_callback:
ret = [format_callback(obj, **kwargs)]
else:
ret = [obj]
children = []
for child in self.children.get(key, ()):
children.extend(self._nested(child, format_callback, **kwargs))
if children:
ret.append(children)
return ret
def nested(self, format_callback=None, **kwargs):
"""
Return the graph as a nested list.
Passes **kwargs back to the format_callback as kwargs.
"""
roots = []
for key in self.seen.keys():
if key not in self.parents:
roots.extend(self._nested(key, format_callback, **kwargs))
return roots
def model_format_dict(obj):
"""
Return a `dict` with keys 'verbose_name' and 'verbose_name_plural',
typically for use with string formatting.
`obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
"""
if isinstance(obj, (models.Model, models.base.ModelBase)):
opts = obj._meta
elif isinstance(obj, models.query.QuerySet):
opts = obj.model._meta
else:
opts = obj
return {
'verbose_name': force_unicode(opts.verbose_name),
'verbose_name_plural': force_unicode(opts.verbose_name_plural)
}
def model_ngettext(obj, n=None):
"""
Return the appropriate `verbose_name` or `verbose_name_plural` value for
`obj` depending on the count `n`.
`obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance.
If `obj` is a `QuerySet` instance, `n` is optional and the length of the
`QuerySet` is used.
"""
if isinstance(obj, models.query.QuerySet):
if n is None:
n = obj.count()
obj = obj.model
d = model_format_dict(obj)
singular, plural = d["verbose_name"], d["verbose_name_plural"]
return ungettext(singular, plural, n or 0)
def lookup_field(name, obj, model_admin=None):
opts = obj._meta
try:
f = opts.get_field(name)
except models.FieldDoesNotExist:
# For non-field values, the value is either a method, property or
# returned via a callable.
if callable(name):
attr = name
value = attr(obj)
elif (model_admin is not None and hasattr(model_admin, name) and
not name == '__str__' and not name == '__unicode__'):
attr = getattr(model_admin, name)
value = attr(obj)
else:
attr = getattr(obj, name)
if callable(attr):
value = attr()
else:
value = attr
f = None
else:
attr = None
value = getattr(obj, name)
return f, attr, value
def label_for_field(name, model, model_admin=None, return_attr=False):
attr = None
try:
label = model._meta.get_field_by_name(name)[0].verbose_name
except models.FieldDoesNotExist:
if name == "__unicode__":
label = force_unicode(model._meta.verbose_name)
elif name == "__str__":
label = smart_str(model._meta.verbose_name)
else:
if callable(name):
attr = name
elif model_admin is not None and hasattr(model_admin, name):
attr = getattr(model_admin, name)
elif hasattr(model, name):
attr = getattr(model, name)
else:
message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name)
if model_admin:
message += " or %s" % (model_admin.__name__,)
raise AttributeError(message)
if hasattr(attr, "short_description"):
label = attr.short_description
elif callable(attr):
if attr.__name__ == "<lambda>":
label = "--"
else:
label = pretty_name(attr.__name__)
else:
label = pretty_name(name)
if return_attr:
return (label, attr)
else:
return label
def display_for_field(value, field):
from django.contrib.admin.templatetags.admin_list import _boolean_icon
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
if field.flatchoices:
return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE)
# NullBooleanField needs special-case null-handling, so it comes
# before the general null test.
elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField):
return _boolean_icon(value)
elif value is None:
return EMPTY_CHANGELIST_VALUE
elif isinstance(field, models.DateField) or isinstance(field, models.TimeField):
return formats.localize(value)
elif isinstance(field, models.DecimalField):
return formats.number_format(value, field.decimal_places)
elif isinstance(field, models.FloatField):
return formats.number_format(value)
else:
return smart_unicode(value)
|