Django外键关联跳转、model对象拷贝、模型字段自定义校验等示例
helper.py 包含外键关联跳转、model对象拷贝、模型字段自定义校验的部分通用函数
from django.utils.safestring import mark_safe from copy import deepcopy from django.utils.translation import ugettext as _ from django.shortcuts import HttpResponseRedirect from django.contrib.admin.options import format_html, messages from django.urls import reverse_lazy from urllib.parse import quote as urlquote from django.core.exceptions import ValidationError link_to_prefix = 'link_to_' # 该前缀与admin.py使用get_related_field函数相结合 def validate_choices(name, value, choices, blank=False): """ 验证model的choices字段选择是否正确, 用于防止在import data时候出现错误数据 :param name: 'x' :param value: 'y' :param choices: ((1, 1), (2, 2)) :param blank: True or False """ try: choice_v = [choice[0] for choice in choices] if not blank or (blank and value): if value not in choice_v: raise ValidationError( message='%(name)s has to be one of %(choice_v)s, not "%(value)s"', code='invalid', params=dict(name=name, choice_v=tuple(choice_v), value=value) ) except Exception as e: raise e def validate_multi_select_choices(name, value, choices, blank=False): """ 验证model的MultiSelectField字段选择是否正确, 用于防止在import data时候出现错误数据 :param name: 'x' :param value: ['Office', ] :param choices: ((1, 1), (2, 2)) :param blank: True or False """ try: choice_v = [choice[0] for choice in choices] values = value if not blank or (blank and value): for v in values: if v not in choice_v: raise ValidationError( message='%(name)s must be in %(choice_v)s, not "%(v)s"', code='invalid', params=dict(name=name, choice_v=tuple(choice_v), v=v) ) except Exception as e: raise e def get_admin_url(instance, admin_prefix='admin', current_app=None): if not instance.pk: return return reverse_lazy( '%s:%s_%s_change' % (admin_prefix, instance._meta.app_label, instance._meta.model_name), args=(instance.pk,), current_app=current_app ) def get_related_field(name, short_description=None, admin_order_field=None, admin_prefix='admin'): """ Create a function that can be attached to a ModelAdmin to use as a list_display field, e.g: client__name = get_related_field('client__name', short_description='Client') """ as_link = name.startswith(link_to_prefix) if as_link: name = name[len(link_to_prefix):] related_names = name.split('__') def getter(self, obj): for related_name in related_names: if not obj: continue obj = getattr(obj, related_name) if obj and as_link: obj = mark_safe('<a href="%s" class="link-with-icon">%s<i class="fa fa-caret-right"></i></a>' % \ (get_admin_url(obj, admin_prefix, current_app=self.admin_site.name), obj)) return obj getter.admin_order_field = admin_order_field or name getter.short_description = short_description or related_names[-1].title().replace('_', ' ') if as_link: getter.allow_tags = True return getter def render_response_copy(func_name, **init_fields): """ Copy可以一个对象, 对于字段值重复率较高的对象适用 :param func_name: 目前只能选择response_change或response_add :param init_fields: 需要初始化的字段 :return view """ def response(self, request, obj, post_url_continue=None): opts = obj._meta if "_save_and_copy" in request.POST: new_obj = deepcopy(obj) new_obj.id = self.model.objects.latest('id').id + 1 for k, v in init_fields.items(): if hasattr(new_obj, k): setattr(new_obj, k, v) new_obj.save() msg_dict = { 'name': opts.verbose_name, 'obj': format_html( '<a href="{}">{}</a>', urlquote(request.path), obj) if func_name == 'response_add' else str(obj), 'new_obj': new_obj, 'action': 'added' if 'response_add' else 'changed' } msg = format_html( _('The {name} "{obj}" was {action} successfully. The following is the copied object {name} "{new_obj}",' ' You may edit it again below.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) redirect_url = reverse_lazy( 'admin:%s_%s_change' % (new_obj._meta.app_label, new_obj._meta.model_name), args=(new_obj.pk,) ) return HttpResponseRedirect(redirect_url) else: return super(self.__class__, self).response_add(request, obj, post_url_continue=post_url_continue) \ if func_name == 'response_add' else \ super(self.__class__, self).response_change(request, obj) return response
models.py 示例
from django.db import models from django.utils.translation import ugettext_lazy as _ from smart_selects.db_fields import ChainedForeignKey from multiselectfield import MultiSelectField SITE_FUNCTION = ( ('Data Center', 'Data Center'), ('Office', 'Office'), ('Factory', 'Factory') ) BUSINESS = ( ('lenovo', 'Lenovo Site'), ('b2b', 'B2B Site'), ('Moto Site', 'Moto Site') ) class Geo(models.Model): id = models.AutoField(primary_key=True, auto_created=True) name = models.CharField(verbose_name=_('Geo'), max_length=80, unique=True) def __str__(self): return self.name class Meta: app_label = 'site' db_table = 'site_geo' verbose_name = _('Geo') verbose_name_plural = _('Geo') class Country(models.Model): id = models.AutoField(primary_key=True, auto_created=True) geo = models.ForeignKey(Geo, on_delete=models.CASCADE) name = models.CharField(verbose_name=_('Country'), max_length=80, unique=True) def __str__(self): return self.name class Meta: app_label = 'site' db_table = 'site_country' verbose_name = _('Country') verbose_name_plural = _('Country') class City(models.Model): id = models.AutoField(primary_key=True, auto_created=True) country = models.ForeignKey(Country, on_delete=models.CASCADE) name = models.CharField(verbose_name=_('City'), max_length=80, unique=True) def __str__(self): return self.name class Meta: unique_together = [('country', 'name'), ] app_label = 'site' db_table = 'site_city' verbose_name = _('City') verbose_name_plural = _('City') class SiteBaseInfo(models.Model): id = models.AutoField(primary_key=True, auto_created=True) geo = models.ForeignKey(Geo, on_delete=models.CASCADE) country = ChainedForeignKey( Country, chained_field='geo', chained_model_field='geo', show_all=False, sort=True ) city = ChainedForeignKey(City, chained_field='country', chained_model_field='country', sort=True) site_name = models.CharField(max_length=32, blank=True, null=True) site_code = models.CharField(max_length=32, unique=True) site_function = MultiSelectField(max_length=255, choices=SITE_FUNCTION, blank=True) business = models.CharField(max_length=16, choices=BUSINESS, blank=True) def clean(self): """ Overwite 自定义字段校验规则 """ try: validate_multi_select_choices('site_function', self.site_function, SITE_FUNCTION, True) validate_choices('business', self.business, BUSINESS) except Exception as e: raise e def __str__(self): return '%s-%s(%s)' % (self.city, self.site_name, self.site_code) class Meta: app_label = 'site' db_table = 'site_base_info' ordering = ['geo', 'country', 'city'] verbose_name = 'Site base information' verbose_name_plural = _('Site base information')
admin.py 示例
from django.contrib import admin from django.utils.translation import ugettext_lazy as _ from import_export.admin import ImportExportModelAdmin from import_export import resources, fields as import_export_fields # django-import-export from .helper import get_related_field from .models import Geo, Country, City, SiteBaseInfo @admin.register(Geo) class GeoAdmin(ImportExportModelAdmin): list_display = ('id', 'name') search_fields = ('id', 'name') fields = ('name',) class CountryResource(resources.ModelResource): class Meta: model = Geo fields = ('id', 'name',) resource_class = CountryResource @admin.register(Country) class CountryAdmin(ImportExportModelAdmin): list_display = ('id', 'link_to_geo', 'name') # TAG1: link_to_geo TAG1与TAG2结合 search_fields = ('id', 'geo__name', 'name') raw_id_fields = ('geo',) list_filter = ('geo',) fields = ('geo', 'name') suit_list_filter_horizontal = ('geo',) ordering = ('geo', 'name') link_to_geo = get_related_field(name='link_to_geo', short_description='Geo') # TAG2: link_to_geo TAG2与TAG1结合 # TAG2等价于下列代码 # def link_to_geo(self, obj): # obj = mark_safe('<a href="%s" class="link-with-icon">%s<i class="fa fa-caret-right"></i></a>' % \ # (get_admin_url(obj.geo, 'admin', current_app=self.admin_site.name), obj.geo)) # return obj # # link_to_geo.admin_order_field = 'geo' # link_to_geo.short_description = 'Geo' # link_to_geo.allow_tags = True class CountryResource(resources.ModelResource): class Meta: model = Country fields = ('id', 'geo', 'name') resource_class = CountryResource @admin.register(City) class CityAdmin(ImportExportModelAdmin): list_display = ('id', 'link_to_country__geo', 'link_to_country', 'name') # TAG1: link_to_country__geo TAG2与TAG1结合 raw_id_fields = ('country',) list_filter = ('country',) suit_list_filter_horizontal = ('country',) search_fields = ('id', 'country__name', 'country__geo__name', 'name') fields = ('country', 'name') ordering = ('country', 'name') link_to_country = get_related_field('link_to_country') # 基于外键的外键链接跳转 link_to_country__geo = get_related_field('link_to_country__geo') # TAG2: link_to_country__geo TAG2与TAG1结合 class CityResource(resources.ModelResource): class Meta: model = City fields = ('id', 'country', 'name') resource_class = CityResource @admin.register(SiteBaseInfo) class SiteBaseInfoAdmin(ImportExportModelAdmin): pass # model对象需要配置以下内容, 且需要更改django模板 # 复制django原生的templates/admin/submit_line.html到自己的工程中, 目录结构保持一直 # templates/admin/submit_line.html 在'// Custom start'与'// Custom end' 之间增加html # 具体内容需考虑使用的django版本(当前django==2.2.15) """ {% load i18n admin_urls %} <div class="submit-row"> {% block submit-row %} {% if show_save %} <input type="submit" value="{% trans 'Save' %}" class="default" name="_save"> {% endif %} {% if show_delete_link and original %} {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %} <p class="deletelink-box"> <a href="{% add_preserved_filters delete_url %}" class="deletelink">{% trans "Delete" %}</a></p> {% endif %} {% if show_save_as_new %} <input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew"> {% endif %} {% if show_save_and_add_another %} <input type="submit" value="{% trans 'Save and add another' %}" name="_addanother"> {% endif %} {% if show_save_and_continue %} <input type="submit" value=" {% if can_change %}{% trans 'Save and continue editing' %}{% else %}{% trans 'Save and view' %}{% endif %}" name="_continue"> {% endif %} // Custom start {% if adminform.model_admin.show_save_and_copy %} <input type="submit" value="Save and copy" class="btn btn-lg btn-info" name="_save_and_copy"> {% endif %} // Custom end {% if show_close %} <a href="{% url opts|admin_urlname:'changelist' %}" class="closelink">{% trans 'Close' %}</a> {% endif %} {% endblock %} </div> """ show_save_and_copy = True response_change = render_response_copy( 'response_change', management_ip='0.0.0.0', serial_number='From copy', other_ip='', remark='', zabbix_monitor_id={}, monitor_node_id_map={}, snmp_ver_auth={}, cannot_backup=False, cannot_audit=False, cannot_backup_reason='', cannot_audit_reason='', last_backup_date=datetime.date(2000, 1, 1), log_check_date=datetime.date(1970, 1, 1) ) response_add = render_response_copy( 'response_add', management_ip='0.0.0.0', serial_number='From copy', other_ip='', remark='', zabbix_monitor_id={}, monitor_node_id_map={}, snmp_ver_auth={}, cannot_backup=False, cannot_audit=False, cannot_backup_reason='', cannot_audit_reason='', last_backup_date=datetime.date(2000, 1, 1), log_check_date=datetime.date(1970, 1, 1) )