首页 > 解决方案 > javascript修改选择字段后的Django ValidationError

问题描述

我正在开发一个用于 REST API 的作业调度应用程序,并且我在 Django 中创建了一个 Tasks、Nodes、URL、Adom、BaseOptions 和 Jobs 表。BaseOptions 具有用于 src_adom 和 dst_adom 的外键,而 Jobs 具有用于 Task、src_node、dst_node、url 和 baseOptions 的外键。我没有使用 ModelForm 而是使用通用表单,只是为处理作业所需的所有字段加载了初始值和选择。我这样做是因为当用户选择不同的节点并想要更改相应 adom 的选择字段时,我无法弄清楚如何更改表单。我正在使用 javascript 来检测新节点并使用该节点的适当 adom 更改选择字段。如果我更改初始源和/或目标节点并修改 adom 选择列表,则会收到验证错误。我知道这是因为我的选择列表与我最初呈现表单的内容有所不同。

有没有办法在“clean”方法期间更改与 ChoiceField 关联的选项?我似乎无法在文档中找到它或在 github 上挖掘 Django 代码。

楷模:

FMG = 'FMG'
FOS = 'FOS'
NODE_TYPE_CHOICES = (
    (FMG, 'FMG'),
    (FOS, 'FOS'),
)

# Create the types of tasks to run
class TaskType(models.Model):
    name = models.CharField(max_length=200)
    command = models.CharField(max_length=200)
    node_type = models.CharField(max_length=60, choices=NODE_TYPE_CHOICES, default=FMG)

    def __str__(self):
        return self.name

# Create the nodes for tasks to be run on
class Node(models.Model):
    ip = models.GenericIPAddressField(protocol='IPv4')
    name = models.CharField(max_length=200)
    apiname = models.CharField(max_length=200)
    apikey = EncryptedCharField(max_length=200)
    node_type = models.CharField(max_length=60, choices=NODE_TYPE_CHOICES, default=FMG)

    def __str__(self):
        return "{0} ({1})".format(self.name, self.ip)

class FMG_URLManger(models.Manager):

    def get_queryset(self):
        return super(FMG_URLManger, self).get_queryset().filter(node_type='FMG')

class FOS_URLManger(models.Manager):

    def get_queryset(self):
        return super(FOS_URLManger, self).get_queryset().filter(node_type='FOS')

class NodeURL(models.Model):
    name = models.CharField(max_length=200)
    node_type = models.CharField(max_length=60, choices=NODE_TYPE_CHOICES, default=FMG)
    table_id = models.CharField(max_length=100)
    table_url = models.CharField(max_length=200)
    help_text = models.CharField(max_length=400, null=True)
    filename = models.CharField(max_length=200)

    objects = models.Manager()
    fmg_objects = FMG_URLManger()
    fos_objects = FOS_URLManger()

    def __str__(self):
        return self.name

class Adom(models.Model):
    name = models.CharField(max_length=200)
    version = models.CharField(max_length=60)
    fmg = models.ForeignKey(Node, related_name='fmgs', on_delete=models.CASCADE)

    class Meta:
        unique_together = (("name", "version", "fmg"),)

    def __str__(self):
        return self.name

class BaseOption(models.Model):
    src_adom = models.ForeignKey(Adom, related_name="src_adom", on_delete=models.CASCADE)
    dst_adom = models.ForeignKey(Adom, related_name="dst_adom", on_delete=models.CASCADE, null=True)
    name_filter = models.CharField(max_length=100, null=True)
    site_name_filter = models.CharField(max_length=100, null=True)
    policy_id_list_filter = models.CharField(max_length=60, null=True)
    keep_policy_id = models.BooleanField(default=False)
    disable_policy = models.BooleanField(default=False)
    import_only = models.BooleanField(default=False)

    def __str__(self):
        return self.src_adom.name

# Create the actual request for jobs
class Job(models.Model):
    UNKNOWN = 'Unknown'
    PENDING = 'Pending'
    SUCCESS = 'Success'
    FAILURE = 'Failure'
    STATUS_CHOICES = (
        (UNKNOWN, 'Unknown'),
        (PENDING, 'Pending'),
        (SUCCESS, 'Success'),
        (FAILURE, 'Failure'),
    )
    description = models.CharField(max_length=400)
    task = models.ForeignKey(TaskType, related_name='job_task', on_delete=models.CASCADE)
    src_node = models.ForeignKey(Node, related_name='src_node', on_delete=models.CASCADE)
    urls = models.ManyToManyField(NodeURL)
    dst_node = models.ForeignKey(Node, related_name='dst_node', on_delete=models.CASCADE)
    user = models.ForeignKey(User, related_name='jobs', on_delete=models.CASCADE)
    opts = models.OneToOneField(BaseOption, related_name='job', on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)
    run_at = models.DateTimeField(editable=True, default=timezone.now)
    start = models.DateTimeField(editable=True, null=True)
    finished = models.DateTimeField(editable=True, null=True)
    status = models.CharField(max_length=12, choices=STATUS_CHOICES, default=UNKNOWN, editable=True)
    status_text = models.TextField(editable=True, null=True)
    file_location = models.CharField(max_length=4086, editable=True, null=True)

    objects = models.Manager()

    def __str__(self):
        return self.description

ScheduleForm 视图:

class ScheduleView(LoginRequiredMixin, generic.FormView):

    login_url = '/login/'
    redirect_field_name = 'redirect_to'

    template_name = 'tools/schedule.html'
    form_class = ScheduleJobForm

    def post(self, request, *args, **kwargs):
        form = self.form_class(request.POST)
        print("Got a POST request, checking for valid...")
        if form.is_valid():
            # <process form cleaned data>
            taskID_nodeType = form.cleaned_data['task']
            description = form.cleaned_data['description']
            src_nodeID_name = form.cleaned_data['src_node']
            dst_nodeID_name = form.cleaned_data['dst_node']
            urlsID_tableID = form.cleaned_data['urls']
            src_adomID = form.cleaned_data['src_adom']
            dst_adomID = form.cleaned_data['dst_adom']
            name_filter = form.cleaned_data['name_filter']
            site_name_fiter = form.cleaned_data['site_name_fiter']
            policy_id_list_filter = form.cleaned_data['policy_id_list_filter']
            taskID = taskID_nodeType.split('.')[0]
            src_nodeID = src_nodeID_name.split('.')[0]
            dst_nodeID = dst_nodeID_name.split('.')[0]
            # urlsID_tableID was a list, now it's a single URL
            # urlIDs = []
            # for urlID_tableID in urlsID_tableID:
            #     urlID = urlID_tableID.split('.')[0]
            #     print("Got back id {}".format(urlID))
            #     urlIDs.append(urlID)
            urlID = urlsID_tableID.split('.')[0]
            urls = NodeURL.objects.filter(id=urlID)
            # print("List of URLS = {}".format(urls))
            src_node = Node.objects.get(id=src_nodeID)
            dst_node = Node.objects.get(id=dst_nodeID)
            task = TaskType.objects.get(id=taskID)
            src_adom = Adom.objects.get(id=src_adomID)
            dst_adom = Adom.objects.get(id=dst_adomID)
            job = Job(description=description,
                    task=task,
                    src_node=src_node,
                    dst_node=dst_node,
                    user=request.user)
            opts = BaseOption(src_adom=src_adom,
                            dst_adom=dst_adom,
                            name_filter=name_filter,
                            site_name_filter=site_name_fiter,
                            policy_id_list_filter=policy_id_list_filter)
            opts.save()
            job.opts = opts
            job.save()
            for url in urls:
                url.job_set.add(job)
            return HttpResponseRedirect('job-list.html')
        else:
            print("The form was not valid. Return for more processing.")

        return render(request, self.template_name, {'form': form})

ScheduleJobForm(带有我未完成的“干净”代码):

class ScheduleJobForm(forms.Form):

    def clean(self):

        print("Don't actually check for on src_adom.")
        cleaned_data = super(ScheduleJobForm, self).clean()
        print(cleaned_data)
        adom = self.cleaned_data.get("src_adom")
        print(adom)
        # if adom < 1:
        #     raise forms.ValidationError("You didn't choose a valid ADOM.")

        print("Don't actually check for on dst_adom.")
        cleaned_data = super(ScheduleJobForm, self).clean()
        adom = self.cleaned_data.get("dst_adom")
        print(adom)
        # if adom < 1:
        #     raise forms.ValidationError("You didn't choose a valid ADOM.")

    initTasks = []
    selectedNodeType = ''
    # Get the first node_type because it will be the default selection of tasks.
    for task in TaskType.objects.all().filter(node_type='FMG').order_by('id'):
        if selectedNodeType == '':
            selectedNodeType = task.node_type
        taskChoice = str(task.id) + '.' + task.node_type, task.name
        initTasks.append(taskChoice)
    # Fill in the initial Nodes for the selected task above.
    initNodes = []
    selectedNodeID = 0
    for node in Node.objects.all().filter(node_type=selectedNodeType).order_by('id'):
        nodeChoice = str(node.id) + '.' + node.name, node.name + ' (' + node.ip + ')'
        if selectedNodeID == 0:
            selectedNodeID = node.id
        initNodes.append(nodeChoice)
    # Also grab the URL's for those node types and fill them in.
    initURLs = []
    for url in NodeURL.objects.all().filter(node_type=selectedNodeType).order_by('table_id'):
        urlChoice = str(url.id) + '.' + url.table_id, url.name
        initURLs.append(urlChoice)
    # Since we've got the first node selected, then all the ADOMs are the same. Get them.
    initAdoms = []
    for adom in Adom.objects.all().filter(fmg_id=selectedNodeID).order_by('id'):
        adomChoice = adom.id, adom.name
        initAdoms.append(adomChoice)
    # Add some hidden fields for the jQuery script to examine.
    selected_node_type = forms.CharField(initial=selectedNodeType, widget=forms.HiddenInput)
    # After this, a jQuery will have to keep the select fields updated if they are changed.
    task = forms.ChoiceField(choices=initTasks)
    description = forms.CharField()
    src_node = forms.ChoiceField(choices=initNodes)
    dst_node = forms.ChoiceField(choices=initNodes)
    urls = forms.ChoiceField(choices=initURLs)
    # These fields will have to be updated by the jQuery to get the available ADOMS for the selected
    # Nodes. We also have to create a custom validator since the choices will change via the JS.
    src_adom = forms.ChoiceField(choices=initAdoms)
    dst_adom = forms.ChoiceField(choices=initAdoms)
    name_filter = forms.CharField()
    site_name_fiter = forms.CharField()
    policy_id_list_filter = forms.CharField()

修改 adoms 的 schedule.js 脚本:

/* 
Of cource, I could use the following but I think the code below is
a bit more readable.
$(function(){
    initPage();
}
); */
const coreapi = window.coreapi
const schema = window.schema

$(document).ready(function(){
        initPage();
    }
);

function initPage() {

    console.log("Adding the change code.")

    // Fill in the initial srcAdom
    getSrcAdom()
    // and the initial dstAdom
    getDstAdom()
    // Hide the wait icon
    $('.loading').hide();

    $('#id_task').change(
        function() {
            var currentNodeType = $('#id_selected_node_type').val();
            var selectedOption = $('#id_task option:selected').val();
            var selectedNodeType = selectedOption.split(".").pop();
            if (currentNodeType != selectedNodeType) {
                // The more I look at the options, I think this will have to be locked
                // into FMG only nodes and create another form for just FOS nodes.
            }
        }
    );

    $('#id_src_node').change(
        function() {
            // Need to change the src adoms
            $('.loading').show();
            getSrcAdom()
            $('.loading').hide();
        }
    );

    $('#id_dst_node').change(
        function() {
            // Need to change the src adoms
            $('.loading').show();
            getDstAdom()
            $('.loading').hide();
        }
    );

}

function getSrcAdom() {
    var selectedOption = $('#id_src_node option:selected').val();
    var selectedNodeName = selectedOption.split(".").pop();

    // Clear the src adom options.
    $('#id_src_adom')
    .find('option')
    .remove();

    // Initialize a client
    var client = new coreapi.Client()

    // Interact with the API endpoint
    var action = ["adom", "list"]
    var params = {
        search: selectedNodeName,
    }
    client.action(schema, action, params).then(function(result) {
        // Return value is in 'result'
        $.each(result, function(index, obj) {
            $('#id_src_adom')
            .append('<option value="' + obj["id"] + '">' + obj["name"] + '</option>');
            console.log(index + " got '" + obj["name"] + "' (id: " + obj["id"] + ")");
        })
    })
}

function getDstAdom() {
    var selectedOption = $('#id_dst_node option:selected').val();
    var selectedNodeName = selectedOption.split(".").pop();

    // Clear the src adom options.
    $('#id_dst_adom')
    .find('option')
    .remove();

    // Initialize a client
    var client = new coreapi.Client()

    // Interact with the API endpoint
    var action = ["adom", "list"]
    var params = {
        search: selectedNodeName,
    }
    client.action(schema, action, params).then(function(result) {
        // Return value is in 'result'
        $.each(result, function(index, obj) {
            $('#id_dst_adom')
            .append('<option value="' + obj["id"] + '">' + obj["name"] + '</option>');
            console.log(index + " got '" + obj["name"] + "' (id: " + obj["id"] + ")");
        })
    })
}

function createNodeSelect(data) {
    // This was to clear all selected nodes and list new node types.
    // It's not going to be used in this form at this time.
    // Need to determine data format from JSON return.
    // Leaving for future code example of how to clear options
    // and replace it with a single option.
    $('#id_src_node')
    .find('option')
    .remove()
    .end()
    .append('<option value="whatever">text</option>')
    .val('whatever');
}

schedule.html 模板(带有 Bootstrap 4 标签):

{% extends "tools/base_site.html" %}

{% block extra_js %}
    {% load static %}
        <script src="{% static 'rest_framework/js/coreapi-0.1.1.js' %}"></script>
        <script src="{% url 'api-docs:schema-js' %}"></script>
        <script src="{% static 'js/schedule.js' %}"></script>
{% endblock %}

{% block content %}
        <div id="content-main">
            <br>
            {% block loading %}
            {% load static %}
            <div id="loading" class="loading">
                <img src="{% static 'images/spinning-wait-icons/wait30.gif' %}" alt="Wait" />
                <!-- <h3>Loading Data from Server</h3> -->
            </div>
            {% endblock %}
            {% load widget_tweaks %}
            {% if form.errors %}
                <div class="alert alert-primary">
                    <button type="button" class="close" data-dismiss="alert">×</button>
                    {% for field in form %} 
                        {% if field.errors %}
                        <li>{{ field.label }}: {{ field.errors|striptags }}</li>
                        {% endif %}
                    {% endfor %}
                </div>
            {% endif %}
            <form method="post">
                {% csrf_token %}

                {% for hidden_field in form.hidden_fields %}
                    {{ hidden_field }}
                {% endfor %}

                {% for field in form.visible_fields %}
                    <div class="form-group">
                    {{ field.label_tag }}
                    {% render_field field class="form-control" %}
                    {% if field.help_text %}
                        <small class="form-text text-muted">{{ field.help_text }}</small>
                    {% endif %}
                    </div>
                {% endfor %}

                <button type="submit" class="btn btn-primary">Schedule</button>
            </form>
        </div>
{% endblock %}

标签: javascriptpythondjangochoicefieldvalidationerror

解决方案


我必须承认我没有浏览所有代码(很多),但乍一看,我认为您应该将您的选择(intTasks,initNodes...)的初始化移动到您的__init__方法中表单类(ScheduleJobForm)。因为您希望代码在实例化类时运行,而不是在加载模块时运行,对吧?所以:

类 ScheduleJobForm(forms.Form):
    selected_node_type = forms.CharField(initial=selectedNodeType, widget=forms.HiddenInput)
    任务 = forms.ChoiceField(choices=initTasks)
    # 更多字段

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 初始化 initNodes 和其余的
        self.fields["src_node"]=initNodes
        # 和相应的。

该表单不关心 Javascript,因为它是在前端的所有 Javascript 魔术之后在 post 请求中实例化的。因此,Javascript 和您的表单之间不必(也不能)有任何交互。


推荐阅读