| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481 |
- from __future__ import absolute_import
- import django
- from django.db import models
- from django.db.models.sql.query import LOOKUP_SEP
- from django.db.models.deletion import Collector
- from django.db.models.fields.related import ForeignObjectRel
- from django.forms.forms import pretty_name
- from django.utils import formats, six
- 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_text, smart_text, smart_str
- from django.utils.translation import ungettext
- from django.urls.base import reverse
- from django.conf import settings
- from django.forms import Media
- from django.utils.translation import get_language
- from django.contrib.admin.utils import label_for_field, help_text_for_field
- from django import VERSION as version
- import datetime
- import decimal
- if 'django.contrib.staticfiles' in settings.INSTALLED_APPS:
- from django.contrib.staticfiles.templatetags.staticfiles import static
- else:
- from django.templatetags.static import static
- try:
- import json
- except ImportError:
- from django.utils import simplejson as json
- try:
- from django.utils.timezone import template_localtime as tz_localtime
- except ImportError:
- from django.utils.timezone import localtime as tz_localtime
- def xstatic(*tags):
- from .vendors import vendors
- node = vendors
- fs = []
- lang = get_language()
- cls_str = str if six.PY3 else basestring
- for tag in tags:
- try:
- for p in tag.split('.'):
- node = node[p]
- except Exception as e:
- if tag.startswith('xadmin'):
- file_type = tag.split('.')[-1]
- if file_type in ('css', 'js'):
- node = "xadmin/%s/%s" % (file_type, tag)
- else:
- raise e
- else:
- raise e
- if isinstance(node, cls_str):
- files = node
- else:
- mode = 'dev'
- if not settings.DEBUG:
- mode = getattr(settings, 'STATIC_USE_CDN',
- False) and 'cdn' or 'production'
- if mode == 'cdn' and mode not in node:
- mode = 'production'
- if mode == 'production' and mode not in node:
- mode = 'dev'
- files = node[mode]
- files = type(files) in (list, tuple) and files or [files, ]
- fs.extend([f % {'lang': lang.replace('_', '-')} for f in files])
- return [f.startswith('http://') and f or static(f) for f in fs]
- def vendor(*tags):
- css = {'screen': []}
- js = []
- for tag in tags:
- file_type = tag.split('.')[-1]
- files = xstatic(tag)
- if file_type == 'js':
- js.extend(files)
- elif file_type == 'css':
- css['screen'] += files
- return Media(css=css, js=js)
- def lookup_needs_distinct(opts, lookup_path):
- """
- Returns True if 'distinct()' should be used to query the given lookup path.
- """
- field_name = lookup_path.split('__', 1)[0]
- field = opts.get_field(field_name)
- if ((hasattr(field, 'remote_field') and
- isinstance(field.remote_field, models.ManyToManyRel)) or
- (is_related_field(field) and
- not field.field.unique)):
- return True
- return False
- def prepare_lookup_value(key, value):
- """
- Returns a lookup value prepared to be used in queryset filtering.
- """
- # if key ends with __in, split parameter into separate values
- if key.endswith('__in'):
- value = value.split(',')
- # if key ends with __isnull, special case '' and false
- if key.endswith('__isnull') and type(value) == str:
- if value.lower() in ('', 'false'):
- value = False
- else:
- value = True
- return value
- 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.
- """
- cls_str = str if six.PY3 else basestring
- if not isinstance(s, cls_str):
- 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().
- """
- cls_str = str if six.PY3 else basestring
- if not isinstance(s, cls_str):
- return s
- 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
- class NestedObjects(Collector):
- def __init__(self, *args, **kwargs):
- super(NestedObjects, self).__init__(*args, **kwargs)
- self.edges = {} # {from_instance: [to_instances]}
- self.protected = set()
- def add_edge(self, source, target):
- self.edges.setdefault(source, []).append(target)
- def collect(self, objs, source_attr=None, **kwargs):
- for obj in objs:
- if source_attr and hasattr(obj, source_attr):
- self.add_edge(getattr(obj, source_attr), obj)
- else:
- self.add_edge(None, obj)
- try:
- return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs)
- except models.ProtectedError as e:
- self.protected.update(e.protected_objects)
- def related_objects(self, related, objs):
- qs = super(NestedObjects, self).related_objects(related, objs)
- return qs.select_related(related.field.name)
- def _nested(self, obj, seen, format_callback):
- if obj in seen:
- return []
- seen.add(obj)
- children = []
- for child in self.edges.get(obj, ()):
- children.extend(self._nested(child, seen, format_callback))
- if format_callback:
- ret = [format_callback(obj)]
- else:
- ret = [obj]
- if children:
- ret.append(children)
- return ret
- def nested(self, format_callback=None):
- """
- Return the graph as a nested list.
- """
- seen = set()
- roots = []
- for root in self.edges.get(None, ()):
- roots.extend(self._nested(root, seen, format_callback))
- 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_text(opts.verbose_name),
- 'verbose_name_plural': force_text(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 is_rel_field(name, model):
- if hasattr(name, 'split') and name.find("__") > 0:
- parts = name.split("__")
- if parts[0] in model._meta.get_all_field_names():
- return True
- return False
- 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 name not in ('__str__', '__unicode__')
- ):
- attr = getattr(model_admin, name)
- value = attr(obj)
- else:
- if is_rel_field(name, obj):
- parts = name.split("__")
- rel_name, sub_rel_name = parts[0], "__".join(parts[1:])
- rel_obj = getattr(obj, rel_name)
- if rel_obj is not None:
- return lookup_field(sub_rel_name, rel_obj, model_admin)
- 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 admin_urlname(value, arg):
- return 'xadmin:%s_%s_%s' % (value.app_label, value.model_name, arg)
- def boolean_icon(field_val):
- return mark_safe(u'<i class="%s" alt="%s"></i>' % (
- {True: 'fa fa-check-circle text-success', False: 'fa fa-times-circle text-error', None: 'fa fa-question-circle muted'}[field_val], field_val))
- def display_for_field(value, field):
- from xadmin.views.list 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.DateTimeField):
- return formats.localize(tz_localtime(value))
- elif isinstance(field, (models.DateField, 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)
- elif isinstance(field.remote_field, models.ManyToManyRel):
- return ', '.join([smart_text(obj) for obj in value.all()])
- else:
- return smart_text(value)
- def display_for_value(value, boolean=False):
- from xadmin.views.list import EMPTY_CHANGELIST_VALUE
- if boolean:
- return boolean_icon(value)
- elif value is None:
- return EMPTY_CHANGELIST_VALUE
- elif isinstance(value, datetime.datetime):
- return formats.localize(tz_localtime(value))
- elif isinstance(value, (datetime.date, datetime.time)):
- return formats.localize(value)
- elif isinstance(value, (decimal.Decimal, float)):
- return formats.number_format(value)
- else:
- return smart_text(value)
- class NotRelationField(Exception):
- pass
- def get_model_from_relation(field):
- if field.related_model:
- return field.related_model
- elif is_related_field(field):
- return field.model
- elif getattr(field, 'remote_field'): # or isinstance?
- return field.remote_field.to
- else:
- raise NotRelationField
- def reverse_field_path(model, path):
- """ Create a reversed field path.
- E.g. Given (Order, "user__groups"),
- return (Group, "user__order").
- Final field must be a related model, not a data field.
- """
- reversed_path = []
- parent = model
- pieces = path.split(LOOKUP_SEP)
- for piece in pieces:
- field = parent._meta.get_field(piece)
- direct = not field.auto_created or field.concrete
- # skip trailing data field if extant:
- if len(reversed_path) == len(pieces) - 1: # final iteration
- try:
- get_model_from_relation(field)
- except NotRelationField:
- break
- if direct:
- related_name = field.related_query_name()
- parent = field.rel.to
- else:
- related_name = field.field.name
- parent = field.model
- reversed_path.insert(0, related_name)
- return (parent, LOOKUP_SEP.join(reversed_path))
- def get_fields_from_path(model, path):
- """ Return list of Fields given path relative to model.
- e.g. (ModelX, "user__groups__name") -> [
- <django.db.models.fields.related.ForeignKey object at 0x...>,
- <django.db.models.fields.related.ManyToManyField object at 0x...>,
- <django.db.models.fields.CharField object at 0x...>,
- ]
- """
- pieces = path.split(LOOKUP_SEP)
- fields = []
- for piece in pieces:
- if fields:
- parent = get_model_from_relation(fields[-1])
- else:
- parent = model
- fields.append(parent._meta.get_field(piece))
- return fields
- def remove_trailing_data_field(fields):
- """ Discard trailing non-relation field if extant. """
- try:
- get_model_from_relation(fields[-1])
- except NotRelationField:
- fields = fields[:-1]
- return fields
- def get_limit_choices_to_from_path(model, path):
- """ Return Q object for limiting choices if applicable.
- If final model in path is linked via a ForeignKey or ManyToManyField which
- has a `limit_choices_to` attribute, return it as a Q object.
- """
- fields = get_fields_from_path(model, path)
- fields = remove_trailing_data_field(fields)
- limit_choices_to = (
- fields and hasattr(fields[-1], 'remote_field') and
- getattr(fields[-1].remote_field, 'limit_choices_to', None))
- if not limit_choices_to:
- return models.Q() # empty Q
- elif isinstance(limit_choices_to, models.Q):
- return limit_choices_to # already a Q
- else:
- return models.Q(**limit_choices_to) # convert dict to Q
- def sortkeypicker(keynames):
- negate = set()
- for i, k in enumerate(keynames):
- if k[:1] == '-':
- keynames[i] = k[1:]
- negate.add(k[1:])
- def getit(adict):
- composite = [adict[k] for k in keynames]
- for i, (k, v) in enumerate(zip(keynames, composite)):
- if k in negate:
- composite[i] = -v
- return composite
- return getit
- def is_related_field(field):
- return isinstance(field, ForeignObjectRel)
- def is_related_field2(field):
- return (hasattr(field, 'remote_field') and field.remote_field != None) or is_related_field(field)
|