11

Django's django.db.models.URLField uses a django.core.validators.URLValidator:

class URLField(CharField):
    default_validators = [validators.URLValidator()]

Since it does not specify the schemes to accept, URLValidator defaults to this set:

schemes = ['http', 'https', 'ftp', 'ftps']

I want my URLField to accept ssh:// URLs, so I tried this:

class SSHURLField(models.URLField):
  '''URL field that accepts URLs that start with ssh:// only.'''
  default_validators = [URLValidator(schemes=['ssh'])]

However when I try to save a new object with a valid ssh:// URL, I get rejected.

This also happens if I skip inheriting from URLField and inherit from CharField directly: (Edit: Actually this does work after I recreated my database. I'm not sure why the former doesn't work.)

class SSHURLField(models.CharField):
  '''URL field that accepts URLs that start with ssh:// only.'''
  default_validators = [URLValidator(schemes=['ssh'])]

  def __init__(self, *args, **kwargs):
    kwargs['max_length'] = 64
    super(SSHURLField, self).__init__(*args, **kwargs)

When I use the URLValidator directly in a test, it works:

def test_url(url):
  try:
    URLValidator(schemes=['ssh'])(url)
    return True
  except:
    return False

>>> test_url('ssh://example.com/')
True

>>> test_url('http://example.com/')
False
rgov
  • 3,516
  • 1
  • 31
  • 51
  • can you post the traceback? – OregonTrail Jan 20 '17 at 05:22
  • @OregonTrail I'm not hitting an exception, but rather in the admin interface when I enter a `ssh://` URL, I get an "Enter a valid URL" error. – rgov Jan 20 '17 at 05:25
  • can you give an example of an `ssh://` uri that you are entering? change any sensitive info of course – OregonTrail Jan 20 '17 at 05:32
  • I have a feeling it's failing to produce all of the return values here https://github.com/django/django/blob/master/django/core/validators.py#L123 – OregonTrail Jan 20 '17 at 05:34
  • Sure, as above (last code block), `ssh://example.com/` seems to pass the URLValidator's checks when I use it directly. – rgov Jan 20 '17 at 05:40
  • so you're sure that your model is using `SSHURLField`, and you've run migrations? – OregonTrail Jan 20 '17 at 05:41
  • Yes. To completely make sure I just blew away my database, deleted all the migrations, created them anew from scratch, and it still doesn't work. – rgov Jan 20 '17 at 05:48
  • Here's what I would usually do at this point. Open a python repl and run `import django.core.validators as v; print v.__file__`. Then edit that file and put a breakpoint at the top of the function `import pdb; pdb.set_trace()`. Then run the django dev server and hit the breakpoint. You will have an interactive debugger to `s` step through and `n` next through the code. – OregonTrail Jan 20 '17 at 05:57
  • Did you figure this out? I'm actually quite curious as I work with Django often. – OregonTrail Jan 20 '17 at 20:14
  • 3
    This is a known issue with Django. See https://code.djangoproject.com/ticket/25594. I'd try overriding forms.URLField as well as fields.URLField. – Iain Dillingham Dec 14 '17 at 17:14

1 Answers1

11

As @IainDillingham mentioned in the comments, this is a bug in Django: overriding the default_validator of a subclassed ModelField does not necessarily override the default_validator of the FormField to which that base class is associated.

For your example, django.db.models.URLField, we can see its associated form field[0] is django.forms.fields.URLField[1]. So the workaround here is to also override def formfield(...) for your customized SSHURLField, to reference a custom django.forms.fields.URLField subclass with the same validator, like so:

from django.core import validators
from django.db import models
from django.forms.fields import URLField as FormURLField

class SSHURLFormField(FormURLField):
    default_validators = [validators.URLValidator(schemes=['ssh'])]

class SSHURLField(models.URLField):  
    '''URL field that accepts URLs that start with ssh:// only.'''  
    default_validators = [validators.URLValidator(schemes=['ssh'])]  

    def formfield(self, **kwargs):
        return super(SSHURLField, self).formfield(**{
            'form_class': SSHURLFormField,
        })

[0] https://github.com/django/django/blob/e17088a108e604cad23b000a83189fdd02a8a2f9/django/db/models/fields/init.py#L2275,L2293
[1] https://github.com/django/django/blob/e17088a108e604cad23b000a83189fdd02a8a2f9/django/forms/fields.py#L650

Joseph
  • 12,678
  • 19
  • 76
  • 115