9

I'm trying to add a field to a Django model that will represent a list of email addresses. I would like a user to enter a comma separated list of addresses into a form in the admin, which my app will then parse to send out a series of emails.

My current implementation covers the basic idea, but has a significant limitation. In the admin, if I enter a string like foo@example.com, bar@example.com, then it correctly writes this to the database as [u'foo@example.com', u'bar@example.com']. But the admin displays this serialized value instead of the humanized string. More importantly, if I edit and save the record, without making any changes, the same conversion changes [u'foo@example.com', u'bar@example.com'] to [u"[u'foo@example.com'", u"u'bar@example.com']"].

How do I convert the python list representation back to a string for use in the admin? Is that the purpose of the value_to_string method or do I need to make the conversion someplace else?

My current custom model field is as follows:

class EmailListField(models.TextField):
    __metaclass__ = models.SubfieldBase

    def to_python(self, value):
        if not value:
            return
        if isinstance(value, list):
            return value
        return [address.strip() for address in value.split(',')]

    def get_db_prep_value(self, value):
        if not value:
            return
        return ','.join(unicode(s) for s in value)

    def value_to_string(self, obj):
        value = self._get_val_from_obj(obj)
        return self.get_db_prep_value(value)

This is based on the SeparatedValuesField described here: http://www.davidcramer.net/code/181/custom-fields-in-django.html.

AndrewF
  • 6,852
  • 7
  • 29
  • 27
  • does it need to be a list would something like the comma separated list in the docs example - http://docs.djangoproject.com/en/dev/ref/forms/validation/#form-field-default-cleaning work? – JamesO Nov 02 '10 at 13:36
  • Thanks, but that example is for a form field, to validate a user's input. What I need is a model field, to save multiple addresses to the database. – AndrewF Nov 02 '10 at 14:07

4 Answers4

4

I based this off the docs but its a model field instead:

class MultiEmailField(models.TextField):

    def to_python(self, value):

        if not value:
            return None  # []

        cleaned_email_list = list()
        #email_list = filter(None, value.split(','))
        email_list = filter(None, re.split(r';|,\s|\n', value))

        for email in email_list:
            if email.strip(' @;,'):
                cleaned_email_list.append(email.strip(' @;,'))

        print cleaned_email_list
        cleaned_email_list = list(set(cleaned_email_list))

        return ", ".join(cleaned_email_list)

    def validate(self, value, model_instance):
        """Check if value consists only of valid emails."""

        # Use the parent's handling of required fields, etc.
        super(MultiEmailField, self).validate(value, model_instance)

        email_list = value.split(',')

        for email in email_list:
            validate_email(email.strip())
jhonkola
  • 3,385
  • 1
  • 17
  • 32
radtek
  • 34,210
  • 11
  • 144
  • 111
3

The question is dead, but you can do it by adding the particular presentation to your python val

class EmailDomainsListField(models.TextField):
    __metaclass__ = models.SubfieldBase

    class Presentation(list):

        def __unicode__(self):
            return u",".join(self)
        def __str__(self):
            return ",".join(self)
    ...

    def to_python(self, value):
        if not value:
            return
        if isinstance(value, EmailDomainsListField.Presentation):
            return value
        return EmailDomainsListField.Presentation([address.strip() for address in        value.split(',')])    
Thorin Schiffer
  • 2,818
  • 4
  • 25
  • 34
3

I wouldn't do that. I would make whatever your EmailListField is supposed to be associated with be one-to-many with email address fields.

hughdbrown
  • 47,733
  • 20
  • 85
  • 108
  • I see how that's more straightforward, but these email addresses aren't actually related to any other pieces of data in my app. Does it make sense to introduce a new email_address table to the database, just to hold one column? – AndrewF Nov 02 '10 at 16:13
  • 1
    Yes, it is the better way. This will also help in other ways, like searching, etc. – zsquare Nov 02 '10 at 17:48
  • 1
    Gotta agree with zsquare. It is the sensible way to do it. You might also want to look at django's admin.TabularInline handler so your admin app can show the list of emails "in-line" with the Profile object (or whatever it is you expect to associate EmailListField with). – Elf Sternberg Nov 02 '10 at 21:20
  • And make use of django emailFIeld validation! – PhoebeB Nov 02 '10 at 22:28
  • 2
    Okay then, I'll try that approach instead. Thanks everyone for setting me straight. – AndrewF Nov 03 '10 at 13:49
2

Following is model field with validation of each email and correct admin handling. Based on eviltnan and AndrewF answers.

from django.core import validators
from django.db import models


class EmailListField(models.CharField):
    __metaclass__ = models.SubfieldBase

    class EmailListValidator(validators.EmailValidator):
        def __call__(self, value):
            for email in value:
                super(EmailListField.EmailListValidator, self).__call__(email)

    class Presentation(list):

        def __unicode__(self):
            return u", ".join(self)

        def __str__(self):
            return ", ".join(self)

    default_validators = [EmailListValidator()]

    def get_db_prep_value(self, value, *args, **kwargs):
        if not value:
            return
        return ','.join(unicode(s) for s in value)

    def value_to_string(self, obj):
        value = self._get_val_from_obj(obj)
        return self.get_db_prep_value(value)

    def to_python(self, value):
        if not value:
            return
        if isinstance(value, self.Presentation):
            return value
        return self.Presentation([address.strip() for address in value.split(',')])
Cuchac
  • 71
  • 1
  • 2