Fork me on GitHub

django自定制admin后台管理

仿照django自带的admin实现自己的admin组件
https://docs.djangoproject.com/zh-hans/2.2/ref/contrib/admin/

django从runserver到启动成功

运行python manage.py runserver启动
打开manage.py文件可以发现该文件代码非常简单,主要干了两件事情:

  1. settings.py文件的路径存入系统环境变量os.environ.os.environ是一个继承自抽象类_collections_abc.MutableMappingos._Environ类的对象.
  2. 将命令行参数sys.argv传给execute_from_command_line执行.execute_from_command_line使用argv创建一个ManagementUtility对象,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Django_Admin.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)


if __name__ == '__main__':
main()

execute_from_command_line

接着进入execute_from_command_line,execute_from_command_line使用argv创建一个ManagementUtility对象,然后调用其execute方法.

1
2
3
4
5
6
# django/core/management/__init__.py

def execute_from_command_line(argv=None):
"""Run a ManagementUtility."""
utility = ManagementUtility(argv)
utility.execute()

在这时,我们发现由于当前文件import了其他依赖而初试化了apps对象和 settings对象.

由于Importdjango.apps.apps,apps对象会被初始化,这个对象主要负责存储已安装的django app的配置(AppConfig:包括app路径,名称,模块,app_label,显示名称等)以及每个app下的各个model.

1
2
3
4
# django/core/management/__init__.py

from django.apps import apps
from django.conf import settings

1
2
3
# django/apps/registry.py

apps = Apps(installed_apps=None)

由于importdjango.conf.settings而初始化LazySettings类,它继承自LazyObject类,LazyObject通过内嵌函数代理了__bytes__,__str__,__len__,__contains__,__iter__,__delitem__,__setitem__,__getitem__,__hash__,__ne__,__eq__,__class__,__dir__,__bool__方法,使这些方法在被调用之前检查setting是否被加载,如果没有的话,调用self._setup()安装,然后再执行被调用的方法.也就是说所有继承LazyObject的类,在调用上面方法时,会先调用_setup方法,只需要将初始化setting的代码放到_setup里,就可以实现懒加载setting.

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
# django/conf/__init__.py
settings = LazySettings()

# django/utils/functional.py
def new_method_proxy(func):
def inner(self, *args):
if self._wrapped is empty:
self._setup()
return func(self._wrapped, *args)
return inner

# django/utils/functional.py
class LazyObject:
...

# Need to pretend to be the wrapped class, for the sake of objects that
# care about this (especially in equality tests)
__class__ = property(new_method_proxy(operator.attrgetter("__class__")))
__eq__ = new_method_proxy(operator.eq)
__lt__ = new_method_proxy(operator.lt)
__gt__ = new_method_proxy(operator.gt)
__ne__ = new_method_proxy(operator.ne)
__hash__ = new_method_proxy(hash)

# List/Tuple/Dictionary methods support
__getitem__ = new_method_proxy(operator.getitem)
__setitem__ = new_method_proxy(operator.setitem)
__delitem__ = new_method_proxy(operator.delitem)
__iter__ = new_method_proxy(iter)
__len__ = new_method_proxy(len)
__contains__ = new_method_proxy(operator.contains)

...

execute

继续看ManagementUtility,它的execute方法主要完成了,命令行参数解析运行command

  • 命令行参数解析.首先生成一个继承自ArgumentParserCommandParser类,然后通过CommandParser类对参数进行解析.我们在编写django command时继承的BaseCommand就是通过CommandParser来解析命令行参数的.
  • 运行command.解析出subcommand,分别根据subcommand通过fetch_command函数得到各种command对象,而command对象是通过获取django\core\management\commands目录和每个appmanagement\commands下所有py文件中的Command类得到.调用django.setup()logging进行配置并调用apps.populate(settings.INSTALLED_APPS)配置安装app.然后由于Command类都继承自BaseCommand,只需要调用run_from_argv方法即可对子命令行参数执行.
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
# django/core/management/__init__.py

def execute(self):
"""
Given the command-line arguments, figure out which subcommand is being
run, create a parser appropriate to that command, and run it.
"""
try:
subcommand = self.argv[1]
except IndexError:
subcommand = 'help' # Display help if no arguments were given.

# Preprocess options to extract --settings and --pythonpath.
# These options could affect the commands that are available, so they
# must be processed early.
parser = CommandParser(usage='%(prog)s subcommand [options] [args]', add_help=False, allow_abbrev=False)
parser.add_argument('--settings')
parser.add_argument('--pythonpath')
parser.add_argument('args', nargs='*') # catch-all
try:
options, args = parser.parse_known_args(self.argv[2:])
handle_default_options(options)
except CommandError:
pass # Ignore any option errors at this point.

try:
settings.INSTALLED_APPS
except ImproperlyConfigured as exc:
self.settings_exception = exc
except ImportError as exc:
self.settings_exception = exc

if settings.configured:
# Start the auto-reloading dev server even if the code is broken.
# The hardcoded condition is a code smell but we can't rely on a
# flag on the command class because we haven't located it yet.
if subcommand == 'runserver' and '--noreload' not in self.argv:
try:
autoreload.check_errors(django.setup)()
except Exception:
# The exception will be raised later in the child process
# started by the autoreloader. Pretend it didn't happen by
# loading an empty list of applications.
apps.all_models = defaultdict(OrderedDict)
apps.app_configs = OrderedDict()
apps.apps_ready = apps.models_ready = apps.ready = True

# Remove options not compatible with the built-in runserver
# (e.g. options for the contrib.staticfiles' runserver).
# Changes here require manually testing as described in
# #27522.
_parser = self.fetch_command('runserver').create_parser('django', 'runserver')
_options, _args = _parser.parse_known_args(self.argv[2:])
for _arg in _args:
self.argv.remove(_arg)

# In all other cases, django.setup() is required to succeed.
else:
django.setup()

self.autocomplete()

if subcommand == 'help':
if '--commands' in args:
sys.stdout.write(self.main_help_text(commands_only=True) + '\n')
elif not options.args:
sys.stdout.write(self.main_help_text() + '\n')
else:
self.fetch_command(options.args[0]).print_help(self.prog_name, options.args[0])
# Special-cases: We want 'django-admin --version' and
# 'django-admin --help' to work, for backwards compatibility.
elif subcommand == 'version' or self.argv[1:] == ['--version']:
sys.stdout.write(django.get_version() + '\n')
elif self.argv[1:] in (['--help'], ['-h']):
sys.stdout.write(self.main_help_text() + '\n')
else:
self.fetch_command(subcommand).run_from_argv(self.argv)

autoreload.check_errors(django.setup)()其实也是调用django.setup方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def setup(set_prefix=True):
"""
Configure the settings (this happens as a side effect of accessing the
first setting), configure logging and populate the app registry.
Set the thread-local urlresolvers script prefix if `set_prefix` is True.
"""
from django.apps import apps
from django.conf import settings
from django.urls import set_script_prefix
from django.utils.log import configure_logging

configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
if set_prefix:
set_script_prefix(
'/' if settings.FORCE_SCRIPT_NAME is None else settings.FORCE_SCRIPT_NAME
)
apps.populate(settings.INSTALLED_APPS)

apps.populate(settings.INSTALLED_APPS)

apps.populate(settings.INSTALLED_APPS)将会根据settings.INSTALLED_APPS中的配置查找app,之后分别调用每个app_configready方法.由于apps.populate是加了锁防止重复执行的,所以每个appapp_config.ready只会初始化一次,我们如果有需要在django启动时执行一次的操作,可以将其放入app_config.ready中.

在调用app_config.ready之前,会先将所有的models加载进来.其通过MODELS_MODULE_NAME="models"; import_module(app_name,MODELS_MODULE_NAME)的方式导入models.

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
def populate(self, installed_apps=None):
"""
Load application configurations and models.

Import each application module and then each model module.

It is thread-safe and idempotent, but not reentrant.
"""
if self.ready:
return

# populate() might be called by two threads in parallel on servers
# that create threads before initializing the WSGI callable.
with self._lock:
if self.ready:
return

# An RLock prevents other threads from entering this section. The
# compare and set operation below is atomic.
if self.loading:
# Prevent reentrant calls to avoid running AppConfig.ready()
# methods twice.
raise RuntimeError("populate() isn't reentrant")
self.loading = True

# Phase 1: initialize app configs and import app modules.
for entry in installed_apps:
if isinstance(entry, AppConfig):
app_config = entry
else:
app_config = AppConfig.create(entry)
if app_config.label in self.app_configs:
raise ImproperlyConfigured(
"Application labels aren't unique, "
"duplicates: %s" % app_config.label)

self.app_configs[app_config.label] = app_config
app_config.apps = self

# Check for duplicate app names.
counts = Counter(
app_config.name for app_config in self.app_configs.values())
duplicates = [
name for name, count in counts.most_common() if count > 1]
if duplicates:
raise ImproperlyConfigured(
"Application names aren't unique, "
"duplicates: %s" % ", ".join(duplicates))

self.apps_ready = True

# Phase 2: import models modules.
for app_config in self.app_configs.values():
app_config.import_models()

self.clear_cache()

self.models_ready = True

# Phase 3: run ready() methods of app configs.
for app_config in self.get_app_configs():
app_config.ready()

self.ready = True
self.ready_event.set()
1
2
3
4
5
6
7
8
def import_models(self):
# Dictionary of models for this app, primarily maintained in the
# 'all_models' attribute of the Apps this AppConfig is attached to.
self.models = self.apps.all_models[self.label]

if module_has_submodule(self.module, MODELS_MODULE_NAME):
models_module_name = '%s.%s' % (self.name, MODELS_MODULE_NAME)
self.models_module = import_module(models_module_name)

AdminConfig

看一下adminAdminConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

class SimpleAdminConfig(AppConfig):
"""Simple AppConfig which does not do automatic discovery."""

default_site = 'django.contrib.admin.sites.AdminSite'
name = 'django.contrib.admin'
verbose_name = _("Administration")

def ready(self):
checks.register(check_dependencies, checks.Tags.admin)
checks.register(check_admin_app, checks.Tags.admin)


class AdminConfig(SimpleAdminConfig):
"""The default AppConfig for admin which does autodiscovery."""

def ready(self):
super().ready()
self.module.autodiscover()

自动扫描每个app下的admin包, 将里面的东西注册到site = DefaultAdminSite()
site是一个单例对象

1
2
def autodiscover():
autodiscover_modules('admin', register_to=site)

autodiscover_modules函数自动加载每个app下的admin.py文件

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
def autodiscover_modules(*args, **kwargs):
"""
Auto-discover INSTALLED_APPS modules and fail silently when
not present. This forces an import on them to register any admin bits they
may want.

You may provide a register_to keyword parameter as a way to access a
registry. This register_to object must have a _registry instance variable
to access it.
"""
from django.apps import apps

register_to = kwargs.get('register_to')
for app_config in apps.get_app_configs():
for module_to_search in args:
# Attempt to import the app's module.
try:
if register_to:
before_import_registry = copy.copy(register_to._registry)

import_module('%s.%s' % (app_config.name, module_to_search))
except Exception:
# Reset the registry to the state before the last import
# as this import will have to reoccur on the next request and
# this could raise NotRegistered and AlreadyRegistered
# exceptions (see #8245).
if register_to:
register_to._registry = before_import_registry

# Decide whether to bubble up this error. If the app just
# doesn't have the module in question, we can ignore the error
# attempting to import it, otherwise we want it to bubble up.
if module_has_submodule(app_config.module, module_to_search):
raise

admin.site.register()

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
def register(self, model_or_iterable, admin_class=None, **options):
"""
Register the given model(s) with the given admin class.

The model(s) should be Model classes, not instances.

If an admin class isn't given, use ModelAdmin (the default admin
options). If keyword arguments are given -- e.g., list_display --
apply them as options to the admin class.

If a model is already registered, raise AlreadyRegistered.

If a model is abstract, raise ImproperlyConfigured.
"""
admin_class = admin_class or ModelAdmin
if isinstance(model_or_iterable, ModelBase):
model_or_iterable = [model_or_iterable]
for model in model_or_iterable:
if model._meta.abstract:
raise ImproperlyConfigured(
'The model %s is abstract, so it cannot be registered with admin.' % model.__name__
)

if model in self._registry:
raise AlreadyRegistered('The model %s is already registered' % model.__name__)

# Ignore the registration if the model has been
# swapped out.
if not model._meta.swapped:
# If we got **options then dynamically construct a subclass of
# admin_class with those **options.
if options:
# For reasons I don't quite understand, without a __module__
# the created class appears to "live" in the wrong place,
# which causes issues later on.
options['__module__'] = __name__
admin_class = type("%sAdmin" % model.__name__, (admin_class,), options)

# Instantiate the admin class to save in the registry
self._registry[model] = admin_class(model, self)
1
2
3
4
5
6
print(admin.site._registry)

{<class 'django.contrib.auth.models.Group'>: <django.contrib.auth.admin.GroupAdmin object at 0x0000019BA62B9BE0>,
<class 'django.contrib.auth.models.User'>: <django.contrib.auth.admin.UserAdmin object at 0x0000019BA62E9BA8>,
<class 'book.models.Book'>: <django.contrib.admin.options.ModelAdmin object at 0x0000019BA62E9DD8>,
<class 'book.models.Author'>: <django.contrib.admin.options.ModelAdmin object at 0x0000019BA62FDC50>}

回到execute函数中

self.fetch_command(subcommand).run_from_argv(self.argv)为重点,是用户输入的命令的真实入口

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
def fetch_command(self, subcommand):
"""
Try to fetch the given subcommand, printing a message with the
appropriate command called from the command line (usually
"django-admin" or "manage.py") if it can't be found.
"""
# Get commands outside of try block to prevent swallowing exceptions
commands = get_commands()
try:
app_name = commands[subcommand]
except KeyError:
if os.environ.get('DJANGO_SETTINGS_MODULE'):
# If `subcommand` is missing due to misconfigured settings, the
# following line will retrigger an ImproperlyConfigured exception
# (get_commands() swallows the original one) so the user is
# informed about it.
settings.INSTALLED_APPS
else:
sys.stderr.write("No Django settings specified.\n")
possible_matches = get_close_matches(subcommand, commands)
sys.stderr.write('Unknown command: %r' % subcommand)
if possible_matches:
sys.stderr.write('. Did you mean %s?' % possible_matches[0])
sys.stderr.write("\nType '%s help' for usage.\n" % self.prog_name)
sys.exit(1)
if isinstance(app_name, BaseCommand):
# If the command is already loaded, use it directly.
klass = app_name
else:
klass = load_command_class(app_name, subcommand)
return klass
1
2
3
4
5
6
7
8
def load_command_class(app_name, name):
"""
Given a command name and an application name, return the Command
class instance. Allow all errors raised by the import process
(ImportError, AttributeError) to propagate.
"""
module = import_module('%s.management.commands.%s' % (app_name, name))
return module.Command()

其中fetch_command根据subcommand返回对应的命令处理文件,load_command_class函数返回module.Command(),在本例中就是self.fetch_command(subcommand)就是runserver.Command()

命令处理文件放在django/core/management/commands下面
打开django/core/management/commands/runserver.py文件
Command类继承了BaseCommand类, 查看BaseCommandrun_from_argv方法

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
def run_from_argv(self, argv):
"""
Set up any environment changes requested (e.g., Python path
and Django settings), then run this command. If the
command raises a ``CommandError``, intercept it and print it sensibly
to stderr. If the ``--traceback`` option is present or the raised
``Exception`` is not ``CommandError``, raise it.
"""
self._called_from_command_line = True
parser = self.create_parser(argv[0], argv[1])

options = parser.parse_args(argv[2:])
cmd_options = vars(options)
# Move positional args out of options to mimic legacy optparse
args = cmd_options.pop('args', ())
handle_default_options(options)
try:
self.execute(*args, **cmd_options)
except Exception as e:
if options.traceback or not isinstance(e, CommandError):
raise

# SystemCheckError takes care of its own formatting.
if isinstance(e, SystemCheckError):
self.stderr.write(str(e), lambda x: x)
else:
self.stderr.write('%s: %s' % (e.__class__.__name__, e))
sys.exit(1)
finally:
try:
connections.close_all()
except ImproperlyConfigured:
# Ignore if connections aren't setup at this point (e.g. no
# configured settings).
pass

execute函数返回的是handle函数

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
def execute(self, *args, **options):
"""
Try to execute this command, performing system checks if needed (as
controlled by the ``requires_system_checks`` attribute, except if
force-skipped).
"""
if options['force_color'] and options['no_color']:
raise CommandError("The --no-color and --force-color options can't be used together.")
if options['force_color']:
self.style = color_style(force_color=True)
elif options['no_color']:
self.style = no_style()
self.stderr.style_func = None
if options.get('stdout'):
self.stdout = OutputWrapper(options['stdout'])
if options.get('stderr'):
self.stderr = OutputWrapper(options['stderr'], self.stderr.style_func)

if self.requires_system_checks and not options.get('skip_checks'):
self.check()
if self.requires_migrations_checks:
self.check_migrations()
output = self.handle(*args, **options)
if output:
if self.output_transaction:
connection = connections[options.get('database', DEFAULT_DB_ALIAS)]
output = '%s\n%s\n%s' % (
self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()),
output,
self.style.SQL_KEYWORD(connection.ops.end_transaction_sql()),
)
self.stdout.write(output)
return output

查看Command类的handle函数, Command重写了父类的handle方法

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
def handle(self, *args, **options):
if not settings.DEBUG and not settings.ALLOWED_HOSTS:
raise CommandError('You must set settings.ALLOWED_HOSTS if DEBUG is False.')

self.use_ipv6 = options['use_ipv6']
if self.use_ipv6 and not socket.has_ipv6:
raise CommandError('Your Python does not support IPv6.')
self._raw_ipv6 = False
if not options['addrport']:
self.addr = ''
self.port = self.default_port
else:
m = re.match(naiveip_re, options['addrport'])
if m is None:
raise CommandError('"%s" is not a valid port number '
'or address:port pair.' % options['addrport'])
self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups()
if not self.port.isdigit():
raise CommandError("%r is not a valid port number." % self.port)
if self.addr:
if _ipv6:
self.addr = self.addr[1:-1]
self.use_ipv6 = True
self._raw_ipv6 = True
elif self.use_ipv6 and not _fqdn:
raise CommandError('"%s" is not a valid IPv6 address.' % self.addr)
if not self.addr:
self.addr = self.default_addr_ipv6 if self.use_ipv6 else self.default_addr
self._raw_ipv6 = self.use_ipv6
self.run(**options)

def run(self, **options):
"""Run the server, using the autoreloader if needed."""
use_reloader = options['use_reloader']

if use_reloader:
autoreload.run_with_reloader(self.inner_run, **options)
else:
self.inner_run(None, **options)

def inner_run(self, *args, **options):
# If an exception was silenced in ManagementUtility.execute in order
# to be raised in the child process, raise it now.
autoreload.raise_last_exception()

threading = options['use_threading']
# 'shutdown_message' is a stealth option.
shutdown_message = options.get('shutdown_message', '')
quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C'

self.stdout.write("Performing system checks...\n\n")
self.check(display_num_errors=True)
# Need to check migrations here, so can't use the
# requires_migrations_check attribute.
self.check_migrations()
now = datetime.now().strftime('%B %d, %Y - %X')
self.stdout.write(now)
self.stdout.write((
"Django version %(version)s, using settings %(settings)r\n"
"Starting development server at %(protocol)s://%(addr)s:%(port)s/\n"
"Quit the server with %(quit_command)s.\n"
) % {
"version": self.get_version(),
"settings": settings.SETTINGS_MODULE,
"protocol": self.protocol,
"addr": '[%s]' % self.addr if self._raw_ipv6 else self.addr,
"port": self.port,
"quit_command": quit_command,
})

try:
handler = self.get_handler(*args, **options)
run(self.addr, int(self.port), handler,
ipv6=self.use_ipv6, threading=threading, server_cls=self.server_cls)
except socket.error as e:
# Use helpful error messages instead of ugly tracebacks.
ERRORS = {
errno.EACCES: "You don't have permission to access that port.",
errno.EADDRINUSE: "That port is already in use.",
errno.EADDRNOTAVAIL: "That IP address can't be assigned to.",
}
try:
error_text = ERRORS[e.errno]
except KeyError:
error_text = e
self.stderr.write("Error: %s" % error_text)
# Need to use an OS exit because sys.exit doesn't work in a thread
os._exit(1)
except KeyboardInterrupt:
if shutdown_message:
self.stdout.write(shutdown_message)
sys.exit(0)

最终通过WSGIServer类启动线程来处理http请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def run(addr, port, wsgi_handler, ipv6=False, threading=False, server_cls=WSGIServer):
server_address = (addr, port)
if threading:
httpd_cls = type('WSGIServer', (socketserver.ThreadingMixIn, server_cls), {})
else:
httpd_cls = server_cls
httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6)
if threading:
# ThreadingMixIn.daemon_threads indicates how threads will behave on an
# abrupt shutdown; like quitting the server by the user or restarting
# by the auto-reloader. True means the server will not wait for thread
# termination before it quits. This will make auto-reloader faster
# and will prevent the need to kill the server manually if a thread
# isn't terminating correctly.
httpd.daemon_threads = True
httpd.set_app(wsgi_handler)
httpd.serve_forever()

启动流程

runserver方式以command的方式启动wsgi. 先初始化appssettings对象;接着获得所有command对象;懒加载调用setting._setup()方法安装配置;接着解析得到子命令,调用django.setup()配置logging和初始化app_config,导入models;根据子命令获取command对象并将参数传递给其并执行,其他参数根据management\commands中的command文件的定义进行解析,然后调用handle方法进行执行;然后调用run方法启动wsgi.

参考:
https://www.cnblogs.com/arrow-kejin/p/10359468.html

默认admin的urls

AdminSite中的urls只分发登录, 推出, 忘记密码等,
models的增删改查相关的操作被另外分发在ModelAdmin

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
# django/contrib/admin/sites.py

class AdminSite:
...

def get_urls(self):
from django.urls import include, path, re_path
# Since this module gets imported in the application's root package,
# it cannot import models from other applications at the module level,
# and django.contrib.contenttypes.views imports ContentType.
from django.contrib.contenttypes import views as contenttype_views

def wrap(view, cacheable=False):
def wrapper(*args, **kwargs):
return self.admin_view(view, cacheable)(*args, **kwargs)
wrapper.admin_site = self
return update_wrapper(wrapper, view)

# Admin-site-wide views.
urlpatterns = [
path('', wrap(self.index), name='index'),
path('login/', self.login, name='login'),
path('logout/', wrap(self.logout), name='logout'),
path('password_change/', wrap(self.password_change, cacheable=True), name='password_change'),
path(
'password_change/done/',
wrap(self.password_change_done, cacheable=True),
name='password_change_done',
),
path('jsi18n/', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'),
path(
'r/<int:content_type_id>/<path:object_id>/',
wrap(contenttype_views.shortcut),
name='view_on_site',
),
]

# Add in each model's views, and create a list of valid URLS for the
# app_index
valid_app_labels = []
for model, model_admin in self._registry.items():
urlpatterns += [
path('%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)),
]
if model._meta.app_label not in valid_app_labels:
valid_app_labels.append(model._meta.app_label)

# If there were ModelAdmins registered, we should have a list of app
# labels for which we need to allow access to the app_index view,
if valid_app_labels:
regex = r'^(?P<app_label>' + '|'.join(valid_app_labels) + ')/$'
urlpatterns += [
re_path(regex, wrap(self.app_index), name='app_list'),
]
return urlpatterns

@property
def urls(self):
return self.get_urls(), 'admin', self.name

...

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
# django/contrib/admin/options.py
class ModelAdmin(BaseModelAdmin):
...

def get_urls(self):
from django.urls import path

def wrap(view):
def wrapper(*args, **kwargs):
return self.admin_site.admin_view(view)(*args, **kwargs)
wrapper.model_admin = self
return update_wrapper(wrapper, view)

info = self.model._meta.app_label, self.model._meta.model_name

urlpatterns = [
path('', wrap(self.changelist_view), name='%s_%s_changelist' % info),
path('add/', wrap(self.add_view), name='%s_%s_add' % info),
path('autocomplete/', wrap(self.autocomplete_view), name='%s_%s_autocomplete' % info),
path('<path:object_id>/history/', wrap(self.history_view), name='%s_%s_history' % info),
path('<path:object_id>/delete/', wrap(self.delete_view), name='%s_%s_delete' % info),
path('<path:object_id>/change/', wrap(self.change_view), name='%s_%s_change' % info),
# For backwards compatibility (was the change url before 1.9)
path('<path:object_id>/', wrap(RedirectView.as_view(
pattern_name='%s:%s_%s_change' % ((self.admin_site.name,) + info)
))),
]
return urlpatterns

@property
def urls(self):
return self.get_urls()

...

admin.site.register

models注册到AdminSite时, 如果没有admin_class, 那么将会使用默认的ModelAdmin, ModelAdmin中定义了一些默认的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...

list_display = ('__str__',)
list_display_links = ()
list_filter = ()
list_select_related = False
list_per_page = 100
list_max_show_all = 200
list_editable = ()
search_fields = ()
date_hierarchy = None
save_as = False
save_as_continue = True
save_on_top = False
paginator = Paginator
preserve_filters = True
inlines = []

...
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
# django/contrib/admin/sites.py

class AdminSite:
...

def register(self, model_or_iterable, admin_class=None, **options):
"""
Register the given model(s) with the given admin class.

The model(s) should be Model classes, not instances.

If an admin class isn't given, use ModelAdmin (the default admin
options). If keyword arguments are given -- e.g., list_display --
apply them as options to the admin class.

If a model is already registered, raise AlreadyRegistered.

If a model is abstract, raise ImproperlyConfigured.
"""
admin_class = admin_class or ModelAdmin
if isinstance(model_or_iterable, ModelBase):
model_or_iterable = [model_or_iterable]
for model in model_or_iterable:
if model._meta.abstract:
raise ImproperlyConfigured(
'The model %s is abstract, so it cannot be registered with admin.' % model.__name__
)

if model in self._registry:
raise AlreadyRegistered('The model %s is already registered' % model.__name__)

# Ignore the registration if the model has been
# swapped out.
if not model._meta.swapped:
# If we got **options then dynamically construct a subclass of
# admin_class with those **options.
if options:
# For reasons I don't quite understand, without a __module__
# the created class appears to "live" in the wrong place,
# which causes issues later on.
options['__module__'] = __name__
admin_class = type("%sAdmin" % model.__name__, (admin_class,), options)

# Instantiate the admin class to save in the registry
self._registry[model] = admin_class(model, self)
...

实现自己的xadmin

这里新建一个app,让它扫描每个app下的xadmin.py文件

1
2
3
4
5
6
7
8
9
from django.apps import AppConfig
from django.utils.module_loading import autodiscover_modules


class XadminConfig(AppConfig):
name = 'xadmin'

def ready(self):
autodiscover_modules('xadmin')

XadminSite

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
class XadminSite(object):
def __init__(self, name="xadmin"):
self._registry = {}
self.name = name

def register(self, model, xadmin_class=None):
if not xadmin_class:
xadmin_class = ModelXadmin

self._registry[model] = xadmin_class(model, self)

def get_urls(self):
temp = []
for model, xadmin_class_obj in self._registry.items():
model_name = model._meta.model_name
app_label = model._meta.app_label
# 分发增删改查
temp.append(path("%s/%s/" % (app_label, model_name), include(xadmin_class_obj.urls)))
return temp

@property
def urls(self):
return self.get_urls(), 'xadmin', self.name


site = XadminSite()

ModelXadmin

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
class ModelXadmin(object):
list_display = ['__str__']

def __init__(self, model, site):
self.model = model
self.site = site

def add(self, request):

return HttpResponse("add")

def delete(self, request, id):
return HttpResponse("delete")

def change(self, request, id):
return HttpResponse("change")

def list_view(self, request):
print(self.model)
print("list_dispaly", self.list_display)

data_list = self.model.objects.all() # 【obj1,obj2,....】

new_data_list = []
for obj in data_list:
temp = []
for filed in self.list_display: # ["pk","name","age",edit]

if callable(filed):
val = filed(self, obj)
else:
val = getattr(obj, filed)

temp.append(val)

new_data_list.append(temp)

print(new_data_list)

return render(request, "list_view.html", locals())

def get_urls(self):

temp = []

model_name = self.model._meta.model_name
app_label = self.model._meta.app_label

temp.append(path("add/", self.add, name="%s_%s_add" % (app_label, model_name)))
temp.append(path("(\d+)/delete/", self.delete, name="%s_%s_delete" % (app_label, model_name)))
temp.append(path("(\d+)/change/", self.change, name="%s_%s_change" % (app_label, model_name)))
temp.append(path("", self.list_view, name="%s_%s_list" % (app_label, model_name)))

return temp

@property
def urls(self):
return self.get_urls()

使用方式和admin一样

xadmin.py
1
2
3
4
5
6
7
8
9
10
11
12
13
from book.models import Book, Author
from xadmin.service import xadmin

from xadmin.service.xadmin import ModelXadmin


class BookCfg(ModelXadmin):
list_display = ('id', 'name')


xadmin.site.register(Book, BookCfg)
xadmin.site.register(Author)
print(xadmin.site._registry)

运行项目,输入url就能看到数据了, ModelXadmin只实现了简单的数据展示功能

表头数据显示

这里先定义一些内置的表头数据.
当该字段是表头时, 显示什么; 不是表头时, 显示的又是什么

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
# 删除 编辑,复选框
def edit(self,obj=None,header=False):
if header:
return "操作"
#return mark_safe("<a href='%s/change'>编辑</a>"%obj.pk)
_url=self.get_change_url(obj)
print("_url",_url)
return mark_safe("<a href='%s'>编辑</a>"%_url)

def deletes(self, obj=None, header=False):
if header:
return "操作"
# return mark_safe("<a href='%s/change'>编辑</a>"%obj.pk)
_url=self.get_delete_url(obj)
return mark_safe("<a href='%s'>删除</a>" % _url)

def checkbox(self,obj=None,header=False):
if header:
return mark_safe('<input id="choice" type="checkbox">')

return mark_safe('<input class="choice_item" type="checkbox">')

def new_list_play(self):
temp=[]
temp.append(ModelXadmin.checkbox)
temp.extend(self.list_display)
if not self.list_display_links:
temp.append(ModelXadmin.edit)
temp.append(ModelXadmin.deletes)
return temp

在传递到模板的时候, 需要进行调整

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
def list_view(self, request):
print(self.model)
print("list_dispaly", self.list_display)

# 构建表头
header_list = []
print("header",
self.new_list_play()) # [checkbox,"pk","name","age",edit ,deletes] 【checkbox ,"__str__", edit ,deletes】

for field in self.new_list_play():

if callable(field):
# header_list.append(field.__name__)
val = field(self, header=True)
header_list.append(val)

else:
if field == "__str__":
header_list.append(self.model._meta.verbose_name.title())
else:
# header_list.append(field)
val = self.model._meta.get_field(field).verbose_name
header_list.append(val)

data_list = self.model.objects.all() # 【obj1,obj2,....】

new_data_list = []
for obj in data_list:
temp = []
for filed in self.new_list_play():

if callable(filed):
val = filed(self, obj)
else:
val = getattr(obj, filed)
if filed in self.list_display_links:
_url = self.get_change_url(obj)
val = mark_safe("<a href='%s'>%s</a>" % (_url, val))

temp.append(val)

new_data_list.append(temp)

print(new_data_list)
# 构建一个添加数据的URL
add_url = self.get_add_url()
return render(request, "list_view.html", locals())


def get_add_url(self):

model_name = self.model._meta.model_name
app_label = self.model._meta.app_label

_url = reverse("xadmin:%s_%s_add" % (app_label, model_name))

return _url

增删改数据

这里使用内置的工厂函数来实现ModelForm

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
# django/forms/models.py

def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
formfield_callback=None, widgets=None, localized_fields=None,
labels=None, help_texts=None, error_messages=None,
field_classes=None):
"""
Return a ModelForm containing form fields for the given model.

``fields`` is an optional list of field names. If provided, include only
the named fields in the returned fields. If omitted or '__all__', use all
fields.

``exclude`` is an optional list of field names. If provided, exclude the
named fields from the returned fields, even if they are listed in the
``fields`` argument.

``widgets`` is a dictionary of model field names mapped to a widget.

``localized_fields`` is a list of names of fields which should be localized.

``formfield_callback`` is a callable that takes a model field and returns
a form field.

``labels`` is a dictionary of model field names mapped to a label.

``help_texts`` is a dictionary of model field names mapped to a help text.

``error_messages`` is a dictionary of model field names mapped to a
dictionary of error messages.

``field_classes`` is a dictionary of model field names mapped to a form
field class.
"""
# Create the inner Meta class. FIXME: ideally, we should be able to
# construct a ModelForm without creating and passing in a temporary
# inner class.

# Build up a list of attributes that the Meta object will have.
attrs = {'model': model}
if fields is not None:
attrs['fields'] = fields
if exclude is not None:
attrs['exclude'] = exclude
if widgets is not None:
attrs['widgets'] = widgets
if localized_fields is not None:
attrs['localized_fields'] = localized_fields
if labels is not None:
attrs['labels'] = labels
if help_texts is not None:
attrs['help_texts'] = help_texts
if error_messages is not None:
attrs['error_messages'] = error_messages
if field_classes is not None:
attrs['field_classes'] = field_classes

# If parent form class already has an inner Meta, the Meta we're
# creating needs to inherit from the parent's inner meta.
bases = (form.Meta,) if hasattr(form, 'Meta') else ()
Meta = type('Meta', bases, attrs)
if formfield_callback:
Meta.formfield_callback = staticmethod(formfield_callback)
# Give this new form class a reasonable name.
class_name = model.__name__ + 'Form'

# Class attributes for the new form class.
form_class_attrs = {
'Meta': Meta,
'formfield_callback': formfield_callback
}

if (getattr(Meta, 'fields', None) is None and
getattr(Meta, 'exclude', None) is None):
raise ImproperlyConfigured(
"Calling modelform_factory without defining 'fields' or "
"'exclude' explicitly is prohibited."
)

# Instantiate type(form) in order to use the same metaclass as form.
return type(form)(class_name, (form,), form_class_attrs)

调用时需要保证fieldsexclude其中一个不为None

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
def add_view(self, request):
ModelFormDemo = modelform_factory(self.model, fields='__all__')
if request.method == "POST":
form = ModelFormDemo(request.POST)
if form.is_valid():
form.save()
return redirect(self.get_list_url())

return render(request, "add_view.html", locals())

form = ModelFormDemo()

return render(request, "add_view.html", locals())

def delete_view(self, request, object_id):
url = self.get_list_url()
if request.method == "POST":
self.model.objects.filter(pk=object_id).delete()
return redirect(url)

return render(request, "delete_view.html", locals())

def change_view(self, request, object_id):
ModelFormDemo = modelform_factory(self.model, fields='__all__')
edit_obj = self.model.objects.filter(pk=object_id).first()

if request.method == "POST":
form = ModelFormDemo(request.POST, instance=edit_obj)
if form.is_valid():
form.save()
return redirect(self.get_list_url())

return render(request, "add_view.html", locals())

form = ModelFormDemo(instance=edit_obj)

return render(request, "change_view.html", locals())

效果图

分页显示

除了list页面, 其他页面并不需要进行分页, 这里将数据进行包装

这里不使用 Django 内置的由分页模块, 而是使用插件 django-pure-pagination

pip install django-pure-pagination

获取当前页显示的数据
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
from pure_pagination import Paginator, EmptyPage, PageNotAnInteger

class ShowList(object):
def __init__(self, config, data_list, request):
self.config = config
self.data_list = data_list
self.request = request

# 分页
data_count = self.data_list.count()
current_page = int(self.request.GET.get("page", 1))
base_path = self.request.path

self.pagination = Paginator(self.data_list, per_page=2, request=request)
self.page_data = self.pagination.page(current_page)


def get_header(self):
# 构建表头
header_list = []
print("header",
self.config.new_list_play())

for field in self.config.new_list_play():

if callable(field):
# header_list.append(field.__name__)
val = field(self.config, header=True)
header_list.append(val)

else:
if field == "__str__":
header_list.append(self.config.model._meta.verbose_name.title())
else:
# header_list.append(field)
val = self.config.model._meta.get_field(field).verbose_name
header_list.append(val)
return header_list

def get_body(self):

new_data_list = []
for obj in self.page_data.object_list:
temp = []
for filed in self.config.new_list_play():

if callable(filed):
val = filed(self.config, obj)
else:
val = getattr(obj, filed)
if filed in self.config.list_display_links:
_url = self.config.get_change_url(obj)
val = mark_safe("<a href='%s'>%s</a>" % (_url, val))
temp.append(val)
new_data_list.append(temp)

return new_data_list

ModelXadmin中使用

数据分页显示视图函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def list_view(self, request):
print(self.model)
print("list_dispaly", self.list_display)

data_list = self.model.objects.all()

showlist = ShowList(self, data_list, request)

header_list = showlist.get_header()
new_data_list = showlist.get_body()

page_data_list = showlist.page_data

# 构建一个添加数据的URL
add_url = self.get_add_url()
return render(request, "list_view.html", locals())

模板使用的是bootstrap

数据分页前台模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<nav aria-label="...">

<ul class="pagination">
{% if page_data_list.has_previous %}
<li class="long"><a href="?{{ page_data_list.previous_page_number.querystring }}">上一页</a></li>
{% endif %}

{% for page in page_data_list.pages %}
{% if page %}
{% ifequal page page_data_list.number %}
<li class="active"><a href="?{{ page.querystring }}">{{ page }}</a></li>
{% else %}
<li><a href="?{{ page.querystring }}" class="page">{{ page }}</a></li>
{% endifequal %}
{% else %}
<li class="none"><a href="">...</a></li>
{% endif %}
{% endfor %}
{% if page_data_list.has_next %}
<li class="long"><a href="?{{ page_data_list.next_page_number.querystring }}">下一页</a></li>
{% endif %}
</ul>

</nav>

效果

搜索框

使用Q()来进行sql条件的拼接

拼接sql语句并进行过滤查询
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
def get_serach_conditon(self, request):
key_word = request.GET.get("q", "")
self.key_word = key_word
from django.db.models import Q
search_connection = Q()
if key_word:
# self.search_fields # ["title","price"]
search_connection.connector = "or"
for search_field in self.search_fields:
search_connection.children.append((search_field + "__contains", key_word))
return search_connection

def list_view(self, request):
print(self.model)
print("list_dispaly", self.list_display)

# 获取serach的Q对象
search_connection = self.get_serach_conditon(request)

# 筛选获取当前表所有数据
data_list = self.model.objects.all().filter(search_connection)

showlist = ShowList(self, data_list, request)
# header_list = showlist.get_header()
# new_data_list = showlist.get_body()
#
# page_data_list = showlist.page_data

# 构建一个添加数据的URL
add_url = self.get_add_url()
return render(request, "list_view.html", locals())
判断是否显示查询框
1
2
3
4
5
{% if showlist.config.search_fields %}
<form action="" class="pull-right">
<input type="text" name="q" value="{{ showlist.config.key_word }}"><button>Go!</button>
</form>
{% endif %}

批处理

原生admin中默认的批处理是删除选中的queryset

这里也直接写一个删除的批处理
修改一下checkbox方法
添加一个批量删除的视图函数

批量删除
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
class ModelXadmin(object):
...

actions = []

def checkbox(self, obj=None, header=False):
if header:
return mark_safe('<input id="choice" type="checkbox">')

return mark_safe('<input class="choice_item" type="checkbox" name="selected_pk" value="%s">'%obj.pk)

...
def queryset_del(self, request, queryset):
queryset.delete()
url = request.path
return redirect(url)

queryset_del.short_description='批量删除'

def list_view(self, request):
print(self.model)
print("list_dispaly", self.list_display)

if request.method == "POST": # action
print("POST:", request.POST)
action = request.POST.get("action")
selected_pk = request.POST.getlist("selected_pk")
action_func = getattr(self, action)
queryset = self.model.objects.filter(pk__in=selected_pk)
ret = action_func(request, queryset)

return ret


# 获取serach的Q对象
search_connection = self.get_serach_conditon(request)

# 筛选获取当前表所有数据
data_list = self.model.objects.all().filter(search_connection)

showlist = ShowList(self, data_list, request)
# header_list = showlist.get_header()
# new_data_list = showlist.get_body()
#
# page_data_list = showlist.page_data

# 构建一个添加数据的URL
add_url = self.get_add_url()
return render(request, "list_view.html", locals())

效果

过滤器

字段类型较多, 需要进行判断属于哪个字段实例(外键, 多对多等)

获取过滤标签链接
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
class ShowList(object):
...

def get_filter_linktags(self):
print("list_filter:", self.config.list_filter)
link_dic = {}
import copy

for filter_field in self.config.list_filter:
params = copy.deepcopy(self.request.GET)
cid = self.request.GET.get(filter_field, 0)

print("filter_field", filter_field)
filter_field_obj = self.config.model._meta.get_field(filter_field)
print("filter_field_obj", filter_field_obj)
print(type(filter_field_obj))
from django.db.models.fields.related import ForeignKey
from django.db.models.fields.related import ManyToManyField
# print("rel...",filter_field_obj.rel.to.objects.all())

if isinstance(filter_field_obj, ForeignKey) or isinstance(filter_field_obj, ManyToManyField):
data_list = filter_field_obj.rel.to.objects.all() # 【publish1,publish2...】
else:
data_list = self.config.model.objects.all().values("pk", filter_field)
print("data_list", data_list)

temp = []
# 处理 全部标签
if params.get(filter_field):
del params[filter_field]
temp.append("<a href='?%s'>全部</a>" % params.urlencode())
else:
temp.append("<a class='active' href='#'>全部</a>")

# 处理 数据标签
for obj in data_list:
if isinstance(filter_field_obj, ForeignKey) or isinstance(filter_field_obj, ManyToManyField):
pk = obj.pk
text = str(obj)
params[filter_field] = pk
else: # data_list= [{"pk":1,"title":"go"},....]
print("========")
pk = obj.get("pk")
text = obj.get(filter_field)
params[filter_field] = text

_url = params.urlencode()
if cid == str(pk) or cid == text:
link_tag = "<a class='active' href='?%s'>%s</a>" % (
_url, text)
else:
link_tag = "<a href='?%s'>%s</a>" % (_url, text)
temp.append(link_tag)

link_dic[self.config.model._meta.get_field(filter_field).verbose_name.upper()] = temp

return link_dic

...

获取过滤sql语句, 并返回已过滤数据
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

class ModelXadmin(object):
list_display = ("__str__", )
list_display_links = ()
modelform_class = None
search_fields = ()
list_filter = ()

actions = []

...

def get_filter_condition(self, request):
from django.db.models import Q
filter_condition = Q()

for filter_field, val in request.GET.items():
if filter_field in self.list_filter:
filter_condition.children.append((filter_field, val))

return filter_condition

def list_view(self, request):
print(self.model)
print("list_dispaly", self.list_display)

if request.method == "POST": # action
print("POST:", request.POST)
action = request.POST.get("action")
selected_pk = request.POST.getlist("selected_pk")
action_func = getattr(self, action)
queryset = self.model.objects.filter(pk__in=selected_pk)
ret = action_func(request, queryset)

return ret

# 获取serach的Q对象
search_connection = self.get_serach_conditon(request)

filter_condition = self.get_filter_condition(request)

# 筛选获取当前表所有数据
data_list = self.model.objects.all().filter(search_connection).filter(filter_condition)

showlist = ShowList(self, data_list, request)
# header_list = showlist.get_header()
# new_data_list = showlist.get_body()
#
# page_data_list = showlist.page_data

# 构建一个添加数据的URL
add_url = self.get_add_url()
return render(request, "list_view.html", locals())

...
在前端页面获取过滤器标签
1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="col-md-3">
<div class="filter">
<h4 style="">Filter</h4>
{% for filter_field,linktags in showlist.get_filter_linktags.items %}
<div class="well">
<p>By {{ filter_field }}</p>
{% for link in linktags %}
<p>{{ link|safe }}</p>
{% endfor %}
</div>
{% endfor %}
</div>
</div>

pop功能

添加或修改时, 如果某个字段是一对多或者多对多时, 可以通过点击右边的+来进行添加该字段的类的实例对象,示例

在添加或修改视图中对ModelForm的字段进行判断, 如果是ModelChoiceField的子类, 则添加is_pop属性, 并设置a标签的链接地址; 否则不做处理

添加
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
def add_view(self, request):
ModelFormDemo = modelform_factory(self.model, fields='__all__')
form = ModelFormDemo()

for bfield in form:
# from django.forms.boundfield import BoundField
print(bfield.field) # 字段对象
print("name", bfield.name) # 字段名(字符串)
print(type(bfield.field)) # 字段类型
from django.forms.models import ModelChoiceField
if isinstance(bfield.field, ModelChoiceField):
bfield.is_pop = True

print("=======>", bfield.field.queryset.model) # 一对多或者多对多字段的关联模型表

related_model_name = bfield.field.queryset.model._meta.model_name
related_app_label = bfield.field.queryset.model._meta.app_label

_url = reverse("xadmin:%s_%s_add" % (related_app_label, related_model_name))
bfield.url = _url + "?pop_res_id=id_%s" % bfield.name

if request.method == "POST":
form = ModelFormDemo(request.POST)
if form.is_valid():
obj = form.save()
pop_res_id = request.GET.get("pop_res_id")
if pop_res_id:
res = {"pk": obj.pk, "text": str(obj), "pop_res_id": pop_res_id}
import json
return render(request, "pop.html", {"res": res})
else:
return redirect(self.get_list_url())
return render(request, "add_view.html", locals())
return render(request, "add_view.html", locals())
修改
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
def change_view(self, request, object_id):
ModelFormDemo = modelform_factory(self.model, fields='__all__')
edit_obj = self.model.objects.filter(pk=object_id).first()

form = ModelFormDemo(instance=edit_obj)

for bfield in form:
# from django.forms.boundfield import BoundField
print(bfield.field) # 字段对象
print("name", bfield.name) # 字段名(字符串)
print(type(bfield.field)) # 字段类型
from django.forms.models import ModelChoiceField
if isinstance(bfield.field, ModelChoiceField):
bfield.is_pop = True

print("=======>", bfield.field.queryset.model) # 一对多或者多对多字段的关联模型表

related_model_name = bfield.field.queryset.model._meta.model_name
related_app_label = bfield.field.queryset.model._meta.app_label

_url = reverse("xadmin:%s_%s_add" % (related_app_label, related_model_name))
bfield.url = _url + "?pop_res_id=id_%s" % bfield.name

if request.method == "POST":
form = ModelFormDemo(request.POST, instance=edit_obj)
if form.is_valid():
obj = form.save()
pop_res_id = request.GET.get("pop_res_id")
if pop_res_id:
res = {"pk": obj.pk, "text": str(obj), "pop_res_id": pop_res_id}
import json
return render(request, "pop.html", {"res": res})
else:
return redirect(self.get_list_url())

return render(request, "change_view.html", locals())


return render(request, "change_view.html", locals())

修改html模板

templates/add_view.html
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

<h3>添加页面</h3>

{% include 'form.html' %}

<script>
function pop_response(pk, text, id) {

console.log(pk, text, id);


// 选择哪一个select标签
// option的文本值和value值

var $option = $('<option>'); // <option></option>
$option.html(text); // <option>出版社</option>
$option.val(pk); // <option value=111>出版社</option>
$option.attr("selected", "selected"); // <option value=111>出版社</option>
$("#" + id).append($option)

}
</script>

</body>
</html>
templates/form.html
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
<div class="container">
<div class="row">
<div class="col-md-6 col-xs-8 col-md-offset-3">
<form action="" method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div style="position: relative">
<label for="">{{ field.label }}</label>
{{ field }} <span class=" error pull-right">{{ field.errors.0 }}</span>

{% if field.is_pop %}
<a onclick="pop('{{ field.url }}')" style="position: absolute;right: -30px;top: 20px"><span
style="font-size: 28px">+</span></a>
{% endif %}
</div>
{% endfor %}

<button type="submit" class="btn btn-default pull-right">提交</button>
</form>
</div>
</div>
</div>
<script>
function pop(url) {
window.open(url, "", "width=600,height=400,top=100,left=100")
}
</script>
templates/pop.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>

<script>

window.opener.pop_response('{{ res.pk }}', "{{ res.text }}", '{{ res.pop_res_id }}')
window.close()
</script>

</body>
</html>

-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!
0%