Source code for django_admin_generator.management.commands.admin_generator

import re
import sys

import python_utils
import six
from django.apps.registry import apps
from django.db import models
from django_utils.management.commands import base_command


[docs] def get_models(app): for model in app.get_models(): yield model
[docs] def get_apps(): for app_config in apps.get_app_configs(): yield app_config.name, app_config yield app_config.name.rsplit('.')[-1], app_config
MAX_LINE_WIDTH = 78 INDENT_WIDTH = 4 LIST_FILTER = ( models.DateField, models.DateTimeField, models.ForeignKey, models.BooleanField, ) SEARCH_FIELD_NAMES = ( 'name', 'slug', ) DATE_HIERARCHY_NAMES = ( 'joined_at', 'updated_at', 'created_at', ) PREPOPULATED_FIELD_NAMES = ( 'slug=name', ) DATE_HIERARCHY_THRESHOLD = 250 LIST_FILTER_THRESHOLD = 25 RAW_ID_THRESHOLD = 100 NO_QUERY_DB = False PRINT_IMPORTS = ''' from django.contrib import admin ''' PRINT_ADMIN_CLASS = ''' class %(name)sAdmin(admin.ModelAdmin): %(class_)s ''' PRINT_ADMIN_REGISTRATION_METHOD = ''' def _register(model, admin_class): admin.site.register(model, admin_class) ''' PRINT_ADMIN_REGISTRATION = ''' _register(%(full_name)s, %(name)sAdmin)''' PRINT_ADMIN_REGISTRATION_LONG = ''' _register( %(full_name)s, %(name)sAdmin)''' PRINT_ADMIN_PROPERTY = ''' %(key)s = %(value)s'''
[docs] class AdminApp(object): def __init__(self, app, model_res, **options): self.app = app self.model_res = model_res self.options = options def __iter__(self): for model in get_models(self.app): admin_model = AdminModel(model, **self.options) for model_re in self.model_res: if model_re.search(admin_model.name): break else: if self.model_res: continue yield admin_model def __unicode__(self): return six.u('').join(self._unicode_generator()) def __str__(self): # pragma: no cover if six.PY2: return six.text_type(self).encode('utf-8', 'replace') else: return self.__unicode__() def _unicode_generator(self): yield PRINT_IMPORTS models = dict() modules = dict() module_names = dict() for admin_model in sorted(self, key=lambda x: x.model.__module__): model = admin_model.model module = model.__module__ # Get the module name if it was generated before or use the last # part of the module path name = modules.get(module, module.rsplit('.', 1)[-1]) print(name, module, file=sys.stderr) # If the module name was already used, use the last two parts of # the module path converting `project.spam.models` to `spam_models` if module_names.get(name, module) != module: name = '_'.join(module.rsplit('.', 2)[-2:]) # Store the module name and models for later use. module_names[name] = module modules[module] = name models[admin_model.name] = name for module, name in sorted(modules.items()): yield 'import %s as %s\n' % (module, name) admin_model_names = [] for admin_model in self: yield PRINT_ADMIN_CLASS % dict( name=admin_model.name, class_=admin_model, ) admin_model_names.append(admin_model.name) yield PRINT_ADMIN_REGISTRATION_METHOD for name in admin_model_names: full_name = '%s.%s' % (models[name], name) context = dict(name=name, full_name=full_name) row = PRINT_ADMIN_REGISTRATION % context if len(row) > MAX_LINE_WIDTH: row = PRINT_ADMIN_REGISTRATION_LONG % context yield row def __repr__(self): return '<%s[%s]>' % ( self.__class__.__name__, self.app, )
[docs] class AdminModel(object): PRINTABLE_PROPERTIES = ( 'list_display', 'list_filter', 'raw_id_fields', 'search_fields', 'prepopulated_fields', 'date_hierarchy', ) def __init__( self, model, raw_id_threshold=RAW_ID_THRESHOLD, date_hierarchy_threshold=DATE_HIERARCHY_THRESHOLD, list_filter_threshold=LIST_FILTER_THRESHOLD, search_field_names=SEARCH_FIELD_NAMES, date_hierarchy_names=DATE_HIERARCHY_NAMES, prepopulated_field_names=PREPOPULATED_FIELD_NAMES, no_query_db=NO_QUERY_DB, **options ): self.model = model self.list_display = python_utils.UniqueList() self.list_filter = python_utils.UniqueList() self.raw_id_fields = python_utils.UniqueList() self.search_fields = python_utils.UniqueList() self.prepopulated_fields = {} self.date_hierarchy = None self.search_field_names = search_field_names self.raw_id_threshold = raw_id_threshold self.list_filter_threshold = list_filter_threshold self.date_hierarchy_threshold = date_hierarchy_threshold self.date_hierarchy_names = date_hierarchy_names self.prepopulated_field_names = prepopulated_field_names self.query_db = not no_query_db def __repr__(self): return '<%s[%s]>' % ( self.__class__.__name__, self.name, ) @property def name(self): return self.model.__name__ def _process_many_to_many(self, meta): raw_id_threshold = self.raw_id_threshold for field in meta.local_many_to_many: related_model = self._get_related_model(field) related_objects = related_model.objects.all() if (related_objects[:raw_id_threshold].count() < raw_id_threshold): yield field.name def _process_fields(self, meta): parent_fields = meta.parents.values() for field in meta.fields: name = self._process_field(field, parent_fields) if name: # pragma: no cover yield name @classmethod def _get_related_model(cls, field): # pragma: no cover if hasattr(field, 'remote_field'): related_model = field.remote_field.model elif hasattr(field.related, 'related_model'): related_model = field.related.related_model else: related_model = field.related.model return related_model def _process_foreign_key(self, field): raw_id_threshold = self.raw_id_threshold list_filter_threshold = self.list_filter_threshold max_count = max(list_filter_threshold, raw_id_threshold) related_model = self._get_related_model(field) related_count = related_model.objects.all() related_count = related_count[:max_count].count() if related_count >= raw_id_threshold: self.raw_id_fields.append(field.name) elif related_count < list_filter_threshold: self.list_filter.append(field.name) else: # pragma: no cover pass # Do nothing :) def _process_field(self, field, parent_fields): if field in parent_fields: # pragma: no cover return self.list_display.append(field.name) if isinstance(field, LIST_FILTER): if isinstance(field, models.ForeignKey) and self.query_db: self._process_foreign_key(field) else: self.list_filter.append(field.name) if field.name in self.search_field_names: self.search_fields.append(field.name) return field.name def __str__(self): # pragma: no cover if six.PY2: return six.text_type(self).encode('utf-8', 'replace') else: return self.__unicode__() def __unicode__(self): return six.u('').join(self._unicode_generator()) def _yield_value(self, key, value): if isinstance(value, (list, set, tuple)): return self._yield_tuple(key, tuple(value)) elif isinstance(value, dict): return self._yield_dict(key, value) elif isinstance(value, six.string_types): return self._yield_string(key, value) else: # pragma: no cover raise TypeError('%s is not supported in %r' % (type(value), value)) def _yield_string(self, key, value, converter=repr): return PRINT_ADMIN_PROPERTY % dict( key=key, value=converter(value), ) def _yield_dict(self, key, value): row_parts = [] row = self._yield_string(key, value) if len(row) > MAX_LINE_WIDTH: row_parts.append(self._yield_string(key, '{', str)) for k, v in six.iteritems(value): row_parts.append('%s%r: %r' % (2 * INDENT_WIDTH * ' ', k, v)) row_parts.append(INDENT_WIDTH * ' ' + '}') row = six.u('\n').join(row_parts) return row def _yield_tuple(self, key, value): row_parts = [] row = self._yield_string(key, value) if len(row) > MAX_LINE_WIDTH: row_parts.append(self._yield_string(key, '(', str)) for v in value: row_parts.append(2 * INDENT_WIDTH * ' ' + repr(v) + ',') row_parts.append(INDENT_WIDTH * ' ' + ')') row = six.u('\n').join(row_parts) return row def _unicode_generator(self): self._process() for key in self.PRINTABLE_PROPERTIES: value = getattr(self, key) if value: yield self._yield_value(key, value) def _process(self): meta = self.model._meta qs = self.model.objects.all() if self.query_db: self.raw_id_fields += list(self._process_many_to_many(meta)) field_names = list(self._process_fields(meta)) if self.query_db: threshold = self.list_filter_threshold + 1 for field in field_names: distinct_count = len(qs.only(field).distinct()[:threshold]) if distinct_count <= self.list_filter_threshold: self.list_filter.append(field) if self.query_db: if qs.count() < self.date_hierarchy_threshold: for field_name in self.date_hierarchy_names[::-1]: if field_name in field_names and not self.date_hierarchy: self.date_hierarchy = field_name break for k in sorted(self.prepopulated_field_names): k, vs = k.split('=', 1) vs = vs.split(',') if k in field_names: incomplete = False for v in vs: if v not in field_names: incomplete = True break if not incomplete: self.prepopulated_fields[k] = vs self.processed = True
[docs] class Command(base_command.CustomBaseCommand): help = '''Generate a `admin.py` file for the given app (models)''' can_import_settings = True requires_system_checks = '__all__',
[docs] def add_arguments(self, parser): super(Command, self).add_arguments(parser) parser.add_argument( '-s', '--search-field', action='append', default=SEARCH_FIELD_NAMES, help='Fields named like this will be added to `search_fields`' ) parser.add_argument( '-d', '--date-hierarchy', action='append', default=DATE_HIERARCHY_NAMES, help='A field named like this will be set as `date_hierarchy`' ) parser.add_argument( '--date-hierarchy-threshold', type=int, default=DATE_HIERARCHY_THRESHOLD, metavar='DATE_HIERARCHY_THRESHOLD', help='If a model has less than DATE_HIERARCHY_THRESHOLD items ' 'it will be added to `date_hierarchy`' ) parser.add_argument( '-p', '--prepopulated-fields', action='append', default=PREPOPULATED_FIELD_NAMES, help='These fields will be prepopulated by the other field.' 'The field names can be specified like `spam=eggA,eggB,eggC`' ) parser.add_argument( '-l', '--list-filter-threshold', type=int, default=LIST_FILTER_THRESHOLD, metavar='LIST_FILTER_THRESHOLD', help='If a foreign key/field has less than LIST_FILTER_THRESHOLD ' 'items it will be added to `list_filter`' ) parser.add_argument( '-r', '--raw-id-threshold', type=int, default=RAW_ID_THRESHOLD, metavar='RAW_ID_THRESHOLD', help='If a foreign key has more than RAW_ID_THRESHOLD items ' 'it will be added to `list_filter`' ) parser.add_argument( '-n', '--no-query-db', action="store_true", dest='no_query_db', help='Don\'t query the database in order to decide whether ' 'fields/relationships are added to `list_filter`' ) parser.add_argument( 'app', help='App to generate admin definitions for' ) parser.add_argument( 'models', nargs='*', help='Regular expressions to filter the models by' )
[docs] def warning(self, message): # This replaces the regular warning method from the CustomBaseCommand # since some Django installations capture all logging output # unfortunately sys.stderr.write(message) sys.stderr.write('\n')
[docs] def handle(self, app=None, *args, **kwargs): super(Command, self).handle(*args, **kwargs) installed_apps = dict(get_apps()) app = installed_apps.get(app) if not app: self.warning( 'This command requires an existing app name as ' 'argument' ) self.warning('Available apps:') for app in sorted(installed_apps): self.warning(' %s' % app) sys.exit(1) model_res = [] for model in kwargs.get('models', []): model_res.append(re.compile(model, re.IGNORECASE)) self.handle_app(app, model_res, **kwargs)
[docs] def handle_app(self, app, model_res, **options): print(AdminApp(app, model_res, **options))