How to create an ical duration field in Django? - django

I use a Django model that I register with the admin site. One of the fields of my model represents a duration. I would like to use the DateTimeField, but instead of saving the value to a datetime in the database, I would like to save it as varchar, formatted according to RFC5545 (ical) (e.g., a duration of 1 day 1 hour 1 min 1 sec would be stored as "P1DT1H1M1S"). How would I do this? Should I overwrite the DateTimeField?

You could create a custom Django field for it instead of overwriting DateTimeField.
https://docs.djangoproject.com/en/dev/howto/custom-model-fields/

Yep, just subclass model.Field. And define two methods:
Field.to_python(self, value) - will convert db value to python object.
Field.get_prep_value(self, value) - this is opposite to to_python converts object to db value.

Thanks bakkal and Pol. Below is what I came up with.
from django.db import models
from icalendar.prop import vDuration
from django.forms.widgets import MultiWidget
from django.forms import TextInput, IntegerField
from django.forms.util import flatatt
from django.forms.fields import MultiValueField
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy as _
from django.core import validators
from datetime import timedelta
def is_int(s):
try:
int(s)
return True
except ValueError:
return False
class Widget_LabelInputField(TextInput):
"""
Input widget with label
"""
input_type="numbers"
def __init__(self, labelCaption, attrs=None):
self.labelCaption = labelCaption
super(Widget_LabelInputField, self).__init__(attrs)
def _format_value(self, value):
if is_int(value):
return value
return '0'
def render(self, name, value, attrs=None):
if value is None:
value = '0'
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))
if (self.labelCaption):
typeString = self.labelCaption + ': '
else:
typeString = ''
return mark_safe(u'' + typeString + '<input%s style=\'width: 30px; margin-right: 20px\'/>' % flatatt(final_attrs))
class Widget_DurationField(MultiWidget):
"""
A Widget that splits duration input into two <input type="text"> boxes.
"""
def __init__(self, attrs=None):
widgets = (Widget_LabelInputField(labelCaption='days', attrs=attrs),
Widget_LabelInputField(labelCaption='hours', attrs=attrs),
Widget_LabelInputField(labelCaption='minutes', attrs=attrs),
Widget_LabelInputField(labelCaption='seconds', attrs=attrs)
)
super(Widget_DurationField, self).__init__(widgets, attrs)
def decompress(self, value):
if value:
duration = vDuration.from_ical(value)
return [str(duration.days), str(duration.seconds // 3600), str(duration.seconds % 3600 // 60), str(duration.seconds % 60)]
return [None, None, None, None]
class Forms_DurationField(MultiValueField):
widget = Widget_DurationField
default_error_messages = {
'invalid_day': _(u'Enter a valid day.'),
'invalid_hour': _(u'Enter a valid hour.'),
'invalid_minute': _(u'Enter a valid minute.'),
'invalid_second': _(u'Enter a valid second.')
}
def __init__(self, *args, **kwargs):
errors = self.default_error_messages.copy()
if 'error_messages' in kwargs:
errors.update(kwargs['error_messages'])
fields = (
IntegerField(min_value=-9999, max_value=9999,
error_messages={'invalid': errors['invalid_day']},),
IntegerField(min_value=-9999, max_value=9999,
error_messages={'invalid': errors['invalid_hour']},),
IntegerField(min_value=-9999, max_value=9999,
error_messages={'invalid': errors['invalid_minute']},),
IntegerField(min_value=-9999, max_value=9999,
error_messages={'invalid': errors['invalid_second']},),
)
super(Forms_DurationField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
if data_list:
if data_list[0] in validators.EMPTY_VALUES:
raise ValidationError(self.error_messages['invalid_day'])
if data_list[1] in validators.EMPTY_VALUES:
raise ValidationError(self.error_messages['invalid_hour'])
if data_list[2] in validators.EMPTY_VALUES:
raise ValidationError(self.error_messages['invalid_minute'])
if data_list[3] in validators.EMPTY_VALUES:
raise ValidationError(self.error_messages['invalid_second'])
return vDuration(timedelta(days=data_list[0],hours=data_list[1],minutes=data_list[2],seconds=data_list[3]))
return None
class Model_DurationField(models.Field):
description = "Duration"
def __init__(self, *args, **kwargs):
super(Model_DurationField, self).__init__(*args, **kwargs)
def db_type(self, connection):
return 'varchar(255)'
def get_internal_type(self):
return "Model_DurationField"
def to_python(self, value):
if isinstance(value, vDuration) or value is None:
return value
return vDuration.from_ical(value)
def get_prep_value(self, value):
return value.to_ical()
def formfield(self, **kwargs):
defaults = {
'form_class': Forms_DurationField,
'required': not self.blank,
'label': capfirst(self.verbose_name),
'help_text': self.help_text}
defaults.update(kwargs)
return super(Model_DurationField, self).formfield(**defaults)

Related

Django form is saying field is required despite providing value

I have a form with a required field customer_phone_number and despite passing a phone number in with the data the form isn't valid..
Submitting the form through a view works but not in the shell.
class OrderForm(forms.ModelForm):
class Meta:
model = models.Order
fields = (
'date',
'customer_phone_number',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['customer_phone_number'].required = True
self.fields['customer_phone_number'].widget = PhoneNumberPrefixWidget()
data = {
'date': '2020-04-20',
'customer_phone_number': '+17168567800',
}
form = OrderForm(data=data)
form.is_valid()
print(form.errors) # {'customer_phone_number': ['This field is required.']}
The only thing I can think of is that I'm using a custom widget but I have no idea how to correct this error.
from phonenumber_field.phonenumber import PhoneNumber
from django.forms import Select, TextInput
from django.forms.widgets import MultiWidget
class PhonePrefixSelect(Select):
initial = '+1'
def __init__(self, attrs=None):
choices = [('+1', '+1')]
super().__init__(attrs, choices=sorted(choices, key=lambda item: item[1]))
def render(self, name, value, *args, **kwargs):
return super().render(
name, value or self.initial, *args, **kwargs)
class PhoneNumberPrefixWidget(MultiWidget):
"""
A Widget that splits phone number input into:
- a country select box for phone prefix
- an input for local phone number
"""
def __init__(self, attrs=None):
widgets = (
PhonePrefixSelect(attrs={
'class': 'form-control w-25 mr-2',
'tabindex': '-1'
}),
TextInput(attrs={
'class': 'form-control w-75'
}),
)
super().__init__(widgets, attrs)
def decompress(self, value):
if value:
if type(value) == PhoneNumber:
if value.country_code and value.national_number:
return ["+%d" % value.country_code, value.national_number]
else:
return value.split('.')
return [None, ""]
def value_from_datadict(self, data, files, name):
values = super().value_from_datadict(
data, files, name)
if all(values):
return '%s.%s' % tuple(values)
return ''
Solved by splitting customer_phone_number into two fields: customer_phone_number_0 and customer_phone_number_1

RadioSelect widget does not show labels in MultiWidget

I am trying to build a custom MultiValue field in django that consists of two widgets: RadioSelect and TextInput: if a user chooses 'Other' then they can insert the value there.
Everything works, with one weird exception: the labels for radio buttons are not shown (see picture). Values are rendered ok, but the labels are just not there. What I am doing wrong?
fields.py
from .widgets import OtherSelectorWidget
class OtherModelField(models.CharField):
def __init__(self, *args, **kwargs):
self.inner_choices = kwargs.pop('choices', None)
super().__init__(*args, **kwargs)
def formfield(self, **kwargs):
return OtherFormField(choices=self.inner_choices, **kwargs)
class OtherFormField(MultiValueField):
def __init__(self, **kwargs):
self.choices = kwargs.pop('choices')
self.widget = OtherSelectorWidget(choices=self.choices)
fields = (CharField(), CharField(),)
super().__init__(fields=fields, require_all_fields=False, **kwargs)
def compress(self, data_list):
return str(data_list)
widgets.py
from datetime import date
from django.forms import widgets
class OtherSelectorWidget(widgets.MultiWidget):
def __init__(self, choices=None, attrs=None):
self.choices = choices
_widgets = (
widgets.RadioSelect(choices=choices),
widgets.TextInput(attrs=attrs),
)
super().__init__(_widgets, attrs)
def decompress(self, value):
if value:
return [value[0], value[1]]
return [None, None, ]
def format_output(self, rendered_widgets):
return ''.join(rendered_widgets)
def value_from_datadict(self, data, files, name):
datelist = [
widget.value_from_datadict(data, files, name + '_%s' % i)
for i, widget in enumerate(self.widgets)]
radio_data = self.widgets[0].value_from_datadict(data, files, name + '_0')
text_data = self.widgets[1].value_from_datadict(data, files, name + '_1')
try:
D = [radio_data, text_data]
except ValueError:
return ''
else:
return D
it seems to be a glitch in Django. Here is a link to a ticket: https://code.djangoproject.com/ticket/29200
I dealt with it by adding wrap_label to widget's context:
class OtherSelectorWidget(widgets.MultiWidget):
def get_context(self, name, value, attrs):
con = super().get_context(name, value, attrs)
con['wrap_label'] = True
return con
Then everything is rendered properly

How to test postgresql specific feature such as JsonField in Django?

I would like to use Postgresql specific JSONField in Django, however, I have not found a way to test with sqlite environment.
Any tip for elegant way?
If you are using Django3.1, The JSON1 extension can be used. If you are running a Django version less than 3.1, below code may help:
import json
from django.conf import settings
from django.contrib.postgres.fields import (
JSONField as DjangoJSONField,
ArrayField as DjangoArrayField,
)
from django.db.models import Field
class JSONField(DjangoJSONField):
pass
class ArrayField(DjangoArrayField):
pass
if 'sqlite' in settings.DATABASES['default']['ENGINE']:
class JSONField(Field):
def db_type(self, connection):
return 'text'
def from_db_value(self, value, expression, connection):
if value is not None:
return self.to_python(value)
return value
def to_python(self, value):
if value is not None:
try:
return json.loads(value)
except (TypeError, ValueError):
return value
return value
def get_prep_value(self, value):
if value is not None:
return str(json.dumps(value))
return value
def value_to_string(self, obj):
return self.value_from_object(obj)
class ArrayField(JSONField):
def __init__(self, base_field, size=None, **kwargs):
"""Care for DjanroArrayField's kwargs."""
self.base_field = base_field
self.size = size
super().__init__(**kwargs)
def deconstruct(self):
"""Need to create migrations properly."""
name, path, args, kwargs = super().deconstruct()
kwargs.update({
'base_field': self.base_field.clone(),
'size': self.size,
})
return name, path, args, kwargs
Import the JSONField from this file in your project and it will adjust itself for both SQLite and PostgreSQL.

How to change the value of custom model field displayed in django admin

I have this code:
import re
import six
from django.core.validators import validate_email
from django.db.models import TextField, SubfieldBase
class EmailsListField(TextField):
__metaclass__ = SubfieldBase
email_separator_re = re.compile(r'\s*,\s*')
def to_python(self, value):
if isinstance(value, six.string_types):
return [x for x in self.email_separator_re.split(value) if x]
else:
return list(value)
def validate(self, value, model_instance):
super(EmailsListField, self).validate(value, model_instance)
for email in value:
validate_email(email)
def get_prep_value(self, value):
if isinstance(value, six.string_types):
return value
else:
return ', '.join(value)
It is designed to accept many emails from a text box, validate, and store them. It saves them as text (e.g. "jim#mail.com, lauren#mail.com") in the db. Everything works as expected except in the admin textboxes and in the list view (and presumably elsewhere). The values are displayed as u"['jim#mail.com', 'lauren#mail.com']". This is, of course, an invalid format as well as ugly.
How do I change this so that it displays as 'jim#mail.com, lauren#mail.com' in both the textboxes and list view?
Thanks to #arocks and #eviltnan.
This answer works for me: https://stackoverflow.com/a/20545049/1813277. In short, a custom list subclass is used with overwritten __str__ functions.
This is my end solution:
import re
import six
from django.core.validators import validate_email
from django.db.models import TextField, SubfieldBase
class EmailsListField(TextField):
__metaclass__ = SubfieldBase
email_separator_re = re.compile(r'\s*,\s*')
class AdminList(list):
def __str__(self):
return str(', '.join(self))
def __unicode__(self):
return unicode(', '.join(self))
def to_python(self, value):
if isinstance(value, six.string_types):
return self.AdminList([x for x in self.email_separator_re.split(value) if x])
else:
return self.AdminList(value)
def validate(self, value, model_instance):
super(EmailsListField, self).validate(value, model_instance)
for email in value:
validate_email(email)
def get_prep_value(self, value):
if isinstance(value, six.string_types):
return value
else:
return ', '.join(value)

How can I make all CharField in uppercase direct in model?

I tried to use UpperCase in all my CharField, in all my Django Model.
Today I have some code in my save method:
def save(self, *args, **kwargs):
for field_name in ['razao_social', 'nome_fantasia', 'cidade', 'endereco','bairro', 'uf', 'cli_parc_nomeparc', 'cli_repr_nomerepr']:
val = getattr(self, field_name, False)
if val:
setattr(self, field_name, val.upper())
super(Pessoa, self).save(*args, **kwargs)
But its take some time. There`s any method to put some uppercase=True in my models?
Thanks.
Here is how to override a Django Model Field and make it upper-case as of Django 1.8.
This will:
work by saving the upper-cased value to the database
returns an upper-cased value in the save response.
Here's the code:
from django.db import models
class UpperCaseCharField(models.CharField):
def __init__(self, *args, **kwargs):
super(UpperCaseCharField, self).__init__(*args, **kwargs)
def pre_save(self, model_instance, add):
value = getattr(model_instance, self.attname, None)
if value:
value = value.upper()
setattr(model_instance, self.attname, value)
return value
else:
return super(UpperCaseCharField, self).pre_save(model_instance, add)
If you want to do this in Django rest framework, here's the code:
from rest_framework import serializers
class UpperCaseSerializerField(serializers.CharField):
def __init__(self, *args, **kwargs):
super(UpperCaseSerializerField, self).__init__(*args, **kwargs)
def to_representation(self, value):
value = super(UpperCaseSerializerField, self).to_representation(value)
if value:
return value.upper()
The correct way would be to define custom model field:
from django.db import models
from django.utils.six import with_metaclass
class UpperCharField(with_metaclass(models.SubfieldBase, models.CharField)):
def __init__(self, *args, **kwargs):
self.is_uppercase = kwargs.pop('uppercase', False)
super(UpperCharField, self).__init__(*args, **kwargs)
def get_prep_value(self, value):
value = super(UpperCharField, self).get_prep_value(value)
if self.is_uppercase:
return value.upper()
return value
and use it like so:
class MyModel(models.Model):
razao_social = UpperCharField(max_length=50, uppercase=True)
# next field will not be upper-cased by default (it's the same as CharField)
nome_fantasia = UpperCharField(max_length=50)
# etc..
you also need to resolve south migration issues (if necessary), by adding this code:
from south.modelsinspector import add_introspection_rules
add_introspection_rules([
(
[UpperCharField],
[],
{
"uppercase": ["uppercase", {"default": False}],
},
),
], ["^myapp\.models\.UpperCharField"])
(path in the last line depends on the field class localization. Please read the south docs for explanation.)
Although there's a small downside when you use shell for instance to create model object and save it in variable:
my_object = MyModel.objects.create(razao_social='blah')
print my_object.razao_social
you won't get upper-cased value. You need to retrieve the object from the database. I will update this post, when I find out how to resolve this issue as well.
Instead of defining a custom field, you can also use the RegexValidator:
from django.core.validators import RegexValidator
...
my_field = models.CharField(
max_length=255,
validators=[RegexValidator('^[A-Z_]*$',
'Only uppercase letters and underscores allowed.')],
)
(see Docs)
Here is my dirty and easier solution without having to deal with migrations:
char_fields = [f.name for f in self._meta.fields if isinstance(f, models.CharField) and not getattr(f, 'choices')]
for f in char_fields:
val = getattr(self, f, False)
if val:
setattr(self, f, val.upper())
super(Cliente, self).save(*args, **kwargs)
django 4, just override the save() method of the model
from django.db import models
class MyModel(models.Model):
my_field = models.CharField(max_length=255)
def save(self, *args, **kwargs):
self.my_field = self.my_field.upper()
super().save(*args, **kwargs)