Working with Mutually Exclusive Fields in Django Forms

written by Daniel Schep on 2014-10-08

Republished from the Celerity blog.

Attention Django developers! When soliciting input from users with an HTML form, you want to design forms to be as flexible as possible. That means you build in multiple form fields to support multiple input options, even if you only want the user to fill in one of them.

Having two or more mutually exclusive fields in a form on a webpage/webapp is a pretty frequent scenario developers have to deal with. Unfortunately, Django's forms have no builtin support for this. In this blog post, I will cover the two simple, but ugly, solutions we Django developers often find ourselves using and introduce a library that makes working with such scenarios much simpler.

v1: Helptext & Validation

The most important part of working with mutually exclusive fields is ensuring that only one of the fields is filled in. This can be achieved by a simple check in the form's clean method. The following code snipped demonstrates that check.

def clean(self):
    cleaned_data = super(TestForm, self).clean()

    if not (bool(self.cleaned_data['test_field1']) !=
            bool(self.cleaned_data['test_field2'])):
        raise forms.ValidationError, 'Fill in only one of the two fields'

    return cleaned_data

And here it is in action:


v2: Add Some JavaScript!

As you can see, the main issue with the barebones approach is that the user experience is pretty awful and relies on help text to explain the situation to the user.

A way to simplify the user experience is to add radio buttons infront of the two fields and only allow one field to be enabled at a time. Here is a simple implementation of that using jQuery.

$(document).ready(function() {
    $('#id_test_field1,#id_test_field2').each(function(i, e) {
        $('<input name="test_field" type="radio" value="'+i+'">')
            .insertBefore(e)
            .click(function() {
                $('#id_test_field1,#id_test_field2').attr('disabled', 'disabled');
                $(e).removeAttr('disabled');
            })
            .next('#id_test_field1').prev().click();
    });
});

And here it is in action:


v3: django-xor-formfields

The third approach involved developing a custom field & widget that renders as two widgets in the DOM and has the requisite JavaScript to manage the disabling of unused fields.

The library I developed is available on Github and PyPI.

It consists of two main parts, the MutuallyExclusiveRadioWidget and the MutuallyExclusiveValueField.

The MutuallyExclusiveRadioWidget accepts a list of django widgets and renders them with a radio button in front of each widget. The widgets are all rendered with the same name parameter in the DOM because since all but one of them is disabled by the JavaScript, only one value gets POSTed to the server.

The MutuallyExclusiveValueField must be passed an iterable of fields and a MutuallyExclusiveRadioWidget built with the respective widgets for those fields. The field validates that only one of the subfields is populated and that is the value of the field.

An example of using this field/widget combo looks like this:

test_field = MutuallyExclusiveValueField(
        fields=(forms.IntegerField(), forms.IntegerField()),
        widget=MutuallyExclusiveRadioWidget(widgets=(
            forms.Select(choices=[(1,1), (2,2), (3,3)]),
            forms.TextInput(),
        )))

And here it is in action:


Bonus! Files & URL Normalizaion

The main usecase that prompted developing the django-xor-formfields package was to allow users to upload files by either submitting an URL or a file. So I created the FileOrURLField. It is a subclass of MutuallyExclusiveValueField and automatically uses FileOrURLWidget, a subclass of MutuallyExclusiveRadioWidget.

It supports 3 forms of normalization through the to kwarg:

The later two options allow the code using your form to be completely unaware of how the user submitted their file or URL. If 'url' is specified and the user uploads a file, the field with download the file and store it as media and return the URL for that file. If 'file' is specified and the user submits an URL, the field will download that file into an InMemoryUploadedFile.An example of using this field which normalized to files is:

And here it is in action:


Notes

The code for the demos is available on Github.

Bechdel Test Pass Rates

written by Daniel Schep on 2013-03-08

Made with data from bechdeltest.com. Sources in Gist 5121543

The data is very sparse before 1930ish, leading to some 100% pass rates

Hardening Synology DSM SSL

written by Daniel Schep on 2013-02-05

Synology DSM 4.1 is vulnerable to BEAST and the Lucky Thirteen attacks out of the box. Switching to RC4 ciphers makes these attacks, and any other future CBC-targeting attacks, not work. To fix this these 2 files need to be updated:

/usr/syno/apache/conf/extra/httpd-alt-port-ssl-setting.conf
/usr/syno/apache/conf/extra/httpd-ssl.conf-common

Update them such that the line starting with SSLCipherSuite is replaced with these two lines:

SSLHonorCipherOrder On
SSLCipherSuite RC4-SHA:HIGH:!ADH:!SSLv2

Restart Apache:

/usr/syno/etc/rc.d/S97apache-sys.sh restart
/usr/syno/etc/rc.d/S97apache-user.sh restart

Double check that no other Apache configs contain SSLCipherSuite options:

grep SSLCipher /usr/syno/apache/conf/extra/*

Sources

Tomato & Namecheap Vulnerability

written by Daniel Schep on 2012-12-04

I discovered today that Tomato's Namecheap Dynamic DNS updater support use HTTP with passwords in the GET parameters.

This means your passwords are super easy to sniff on the wire. Don't use it.

Namecheap shouldn't be accepting this sort of request with out HTTPS and Tomato shouldn't be using it.

Here's how I found it:

Password has obviously already been regenerated.

Simple alternative

Curl on any linux box will serve the purpose nicely (note the https://):

curl "https://dynamicdns.park-your-domain.com/update?host=$HOST&domain=$DOMAIN&password=$PASSWORD&ip=`curl -s http://my-ip.heroku.com/`"

Gmvault on Synology NAS

written by Daniel Schep on 2012-11-18

  1. Install Python from DSM Package Center
  2. Enable SSH
  3. SSH in
  4. Enable and switch to user shell (optional, requres user homes to be enabled)
    • set shell to /bin/ash in /etc/passwd
    • su - <user>
  5. Create directory to store all of this (optional)
    • mkdir gmvault&&cd gmvault
  6. Get virtualenv
    • wget https://raw.github.com/pypa/virtualenv/develop/virtualenv.py
  7. Create virtualenv & install gmvault
    • python virtualenv.py venv
    • ./venv/bin/pip install gmvault
  8. Backup Gmail!

Relevant Links: