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)
    )

 

 

 

posted @ 2021-01-08 10:36  士为知己  阅读(168)  评论(0)    收藏  举报