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'' % ( {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") -> [ , , , ] """ 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)