Django URLField with custom scheme - django

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

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

Related

How to set up a maximum value in a Django DurationField?

I have a Django models with a DurationField. I would like that the users cannot put a value over 00:09:59. Unfortunately I could not find any information to do this in the doc : https://docs.djangoproject.com/en/4.1/ref/models/fields/#durationfield
You can add validators for that.
It will look something like:
DurationField(
validators=[MaxValueValidator(datetime.timedelta(minutes=10)]
)
It is not possible with default DurationField.
You can override the clean_%s method in the form:
from django import forms
class YourForm(forms.Form):
duration_field = forms.DurationField()
def clean_duration_field(self):
data = self.cleaned_data['duration_field']
if value > timedelta(minutes=9, seconds=59):
raise forms.ValidationError("....", code="invalid")
return data
If you need to do this validation in many places, you can create a custom Field for overriding the validating method of field:
from django import forms
from django.core.validators import validate_email
class RestrictedDurationField(forms.DurationField):
def __init__(self, limit: timdelta, *args, **kwargs):
super().__init__(*args, **kwargs)
self.limit = limit
def validate(self, value):
super().validate(value)
if value > self.limit:
raise forms.ValidationError("....", code="invalid")
# and usage
class form(Form):
duration_field = RestrictedDurationField(limit=timedelta(minutes=9, seconds=59), ...)

Django custom field widget and widget behaviour for a custom "ListField"

I'm creating the following custom field based off How to create list field in django
import re
from django.db import models
from django.forms.widgets import TextInput
class ListField(models.TextField):
__metaclass__ = models.SubfieldBase
description = "Stores a python list"
widget = TextInput
def __init__(self, *args, **kwargs):
super(ListField, self).__init__(*args, **kwargs)
def to_python(self, value):
if not value:
return []
return filter(None, re.split(r'\,|\s*', value))
def get_prep_value(self, value):
if value is None:
return value
return ', '.join(value)
def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return self.get_db_prep_value(value)
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^cflowportal\.utils\.modelutils\.ListField"])
Basically, what I want to achieve is a field where you write something like "1, asd, asdf fdgd", it stores it as such in the database but when retrieved it should return that string as an array and when given an array it should convert it back to a comma-seperated string.
I'm still not sure if what I've written so far works, but I'm having trouble displaying it as an input field and not a textarea even if I've set widget=TextInput.
So, how do I show it in the admin with the same input used by the standard CharField?
How can I customize it so that it displays a comma-separated string when showed on such input, but is given back as a Python List when accessed elsewhere?
Thanks
The following is a method to realize what you want
from django.db import models
class Blog(models.Model):
title = models.CharField(max_length=256)
labels = models.TextField()
def get_labels(self):
return self.content.split('\n')
def set_labels(self,value):
if isinstance(value,list) or isinstance(value,tuple) or isinstance(value,set):
content = '\n'.join(value)
else:
content = value
self.content = content
You can regard labels as a ListField, set value use obj.set_labels(list) function, and get value use obj.get_labels()
It act as a List Field, and admin site will run as a normal TextField.
This is what I did, but a better solution is excepted.
and a better way to do this is using save_model in admin.py:
class BlogAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
# extra data handling, prevent data convert
obj.save()

How to map model fields with form field in Django

We have one application containing models.py which contains n no. of classes that inherits base class.We want to create form which dynamically takes value from user n saves in db but problem is that we want to use django form fields instead of django model forms.
As we know there are some fields missing in django forms such as PositiveIntegerField, CommaSeparetedIntegerFields etc. How can we achieve this using django form fields?
If we write follwing code in shell.
from djnago.db import models
mvar = models.PositiveIntegerFields()
from django import forms
fvar = forms.PositiveIntegerFields()
AttributeError: 'module' object has no attribute 'PositiveIntegerField'
forms.py
from django import forms
class ContextForm(forms.Form):
def __init__(self, rdict, *args, **kwargs):
super(ContextForm, self).__init__(*args, **kwargs)
for key in rdict.keys():
self.fields['%s' % str(key)] = getattr(forms,rdict.get(key))()
rdict = {'address': 'CharField','phone': 'CharField', 'Salary': 'PositiveIntegerField','first name': 'CharField','last name':'CharField'}
Looking at the source, all the field does is call the default form field with a keyword argument: min_value.
class PositiveIntegerField(IntegerField):
description = _("Positive integer")
def get_internal_type(self):
return "PositiveIntegerField"
def formfield(self, **kwargs):
defaults = {'min_value': 0}
defaults.update(kwargs)
return super(PositiveIntegerField, self).formfield(**defaults)
Therefore what you are looking for is merely
from django import forms
fvar = forms.IntegerField(min_value=0)
fvar.clean(-1)
# ValidationError: [u'Ensure this value is greater than or equal to 0.']
As for CommaSeparatedIntegerField, it looks like a CharField with some django.core.validators.validate_comma_separated_integer_list passed in.
f = forms.CharField(validators=[django.core.validators.validate_comma_separated_integer_list])
f.clean('1,2,3')
All this does is make sure the passed in string is '^[\d,]+$'. The field doesn't even do any python conversions... it doesn't really seem to save much time if just validates form input. Indeed, there's a comment that says "maybe move to contrib". Agreed..
Decided to look into this for fun. Here's a ModelForm generator that overrides model fields with new fields... It doesn't yet handle kwargs. It was just the first method I could think of to do this.. without looking into modelform generation itself. It constructs a regular ModelForm that modifies the form /after/ initialization.
MODEL_FIELD_MAP = {
models.IntegerField: forms.CharField,
# change all IntegerField to forms.CharField
}
def modelform_generator(mymodel):
class MyModelForm(forms.ModelForm):
class Meta:
model = mymodel
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
for name, form_field in self.fields.items():
try:
model_field = self._meta.model._meta.get_field_by_name(name)[0]
# is this a model field?
field_override = MODEL_FIELD_MAP.get(model_field.__class__)
# do we have this model field mapped to a form field?
if field_override:
self.fields[name] = field_override()
# set the form field to the target field class
except models.FieldDoesNotExist:
pass
return MyModelForm

Read-only form field formatting

I want to display a field as read only in a ModelAdmin form, so I added it to the readonly_fields attribute.
However, since the field contains a currency, stored as an integer, I want to apply some nice formatting it. I've created a custom ModelForm for my ModelAdmin, trying to apply the formatting in the overridden __init__ method.
The problem is, I cannot find the value. The field is not present in the self.fields attribute.
Does anyone know where the values for the readonly_fields are kept, or is there a better/different approach?
Just do something like:
class MyAdmin(admin.ModelAdmin):
readonly_fields = ('foo',)
def foo(self, obj):
return '${0}'.format(obj.amount)
An alternate approach, which works for all types of forms is to create a widget to represent a read only field. Here is one that I wrote for my own use. You can change the <span %s>%s</span> to suit your own requirements.
from django import forms
from django.utils.safestring import mark_safe
from django.utils.encoding import force_unicode
class ReadOnlyWidget(forms.TextInput):
def render(self, name, value, attrs=None):
if value is None:
value = ''
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
if value != '':
# Only add the 'value' attribute if a value is non-empty.
final_attrs['value'] = force_unicode(self._format_value(value))
return mark_safe(u'<span%s />%s</span>' % (flatatt(final_attrs),value))
Once you have that added, simply do this:
class MyAdmin(admin.ModelAdmin):
foo = models.TextField(widget=ReadOnlyWidget(attrs={'class':'read-only'}
initial="$50")
Then in your CSS, do some styling for a read-only class, or you can adjust the attributes accordingly.
Another, more appropriate solution, works in Django 2.1.2:
ModelAdmin renders read-only fields via special wrapper AdminReadonlyField (django/contrib/admin/helpers.py) if we look at contents method, we can see
the code
if getattr(widget, 'read_only', False):
return widget.render(field, value)
It means that if a widget has read_only attribute with True value
then the read-only field will invoke widget's render method.
Hence, you can use render method to format your value.
For example:
class CustomDateInput(widgets.DateInput):
read_only = True
def _render(self, template_name, context, renderer=None):
return 'you value'
class CustomForm(forms.ModelForm):
some_field = forms.DateTimeField(widget=CustomDateInput())
#admin.register(SomeModel)
class SomeModelAdmin(admin.ModelAdmin):
form = CustomForm
readonly_fields = ['some_field']

Adding links to full change forms for inline items in django admin?

I have a standard admin change form for an object, with the usual StackedInline forms for a ForeignKey relationship. I would like to be able to link each inline item to its corresponding full-sized change form, as the inline item has inlined items of its own, and I can't nest them.
I've tried everything from custom widgets to custom templates, and can't make anything work. So far, the "solutions" I've seen in the form of snippets just plain don't seem to work for inlines. I'm getting ready to try some DOM hacking with jQuery just to get it working and move on.
I hope I must be missing something very simple, as this seems like such a simple task!
Using Django 1.2.
There is a property called show_change_link since Django 1.8.
I did something like the following in my admin.py:
from django.utils.html import format_html
from django.core.urlresolvers import reverse
class MyModelInline(admin.TabularInline):
model = MyModel
def admin_link(self, instance):
url = reverse('admin:%s_%s_change' % (instance._meta.app_label,
instance._meta.module_name),
args=(instance.id,))
return format_html(u'Edit', url)
# … or if you want to include other fields:
return format_html(u'Edit: {}', url, instance.title)
readonly_fields = ('admin_link',)
The currently accepted solution here is good work, but it's out of date.
Since Django 1.3, there is a built-in property called show_change_link = True that addresses this issue.
This can be added to any StackedInline or TabularInline object. For example:
class ContactListInline(admin.TabularInline):
model = ContactList
fields = ('name', 'description', 'total_contacts',)
readonly_fields = ('name', 'description', 'total_contacts',)
show_change_link = True
The result will be something line this:
I had similar problem and I came up with custom widget plus some tweaks to model form. Here is the widget:
from django.utils.safestring import mark_safe
class ModelLinkWidget(forms.Widget):
def __init__(self, obj, attrs=None):
self.object = obj
super(ModelLinkWidget, self).__init__(attrs)
def render(self, name, value, attrs=None):
if self.object.pk:
return mark_safe(
u'<a target="_blank" href="../../../%s/%s/%s/">%s</a>' %\
(
self.object._meta.app_label,
self.object._meta.object_name.lower(),
self.object.pk, self.object
)
)
else:
return mark_safe(u'')
Now since widget for each inline need to get different object in constructor you can't just set it in standard way, but in Form's init method:
class TheForm(forms.ModelForm):
...
# required=False is essential cause we don't
# render input tag so there will be no value submitted.
link = forms.CharField(label='link', required=False)
def __init__(self, *args, **kwargs):
super(TheForm, self).__init__(*args, **kwargs)
# instance is always available, it just does or doesn't have pk.
self.fields['link'].widget = ModelLinkWidget(self.instance)
Quentin's answer above works, but you also need to specify fields = ('admin_link',)
There is a module for this purpose. Check out:
django-relatives
I think: args=[instance.id] should be args=[instance.pk]. It worked for me!