DickyS
DickyS

Reputation: 115

Empty value for string in model function

First project newbie here - In this model I am trying to create a field that combines first and last names of two people depending if the last names are the same or not. If the last name is the same, I want it to display as "first_name1 & first_name2 last_name1". It works except that when last_name1 is empty, which will be the case a lot of the time, it displays something like "John & Jane None". I had to specify last_name1 as a string or else I got an error: must be str, not NoneType. How do I do this properly? Also what do I call this type of function in a model...is it a manager? I wasn't sure how to title this post.

class Contact(models.Model):
    first_name1 = models.CharField(max_length=100, verbose_name='First Name', null=True)
    last_name1 = models.CharField(max_length=100, verbose_name='Last Name', null=True, blank=True)
    first_name2 = models.CharField(max_length=100, verbose_name='First Name (Second Person)', null=True, blank=True)
    last_name2 = models.CharField(max_length=100, verbose_name='Last Name (Second Person)', null=True, blank=True)

    def get_full_name(self):
        combined_name = ''

        if self.last_name1 == self.last_name2:
            combined_name = self.first_name1 + ' & ' + self.first_name2 + ' ' + str(self.last_name1)

        return '%s' % (combined_name)

    full_name = property(get_full_name)

Upvotes: 1

Views: 993

Answers (3)

schillingt
schillingt

Reputation: 13731

You can check if those values are "Truth-y" before doing the comparison check. However, you'll need to decide how to handle the other cases.

@property
def get_full_name(self):
    combined_name = ''
    if self.last_name1 and self.last_name2:
        if self.last_name1 == self.last_name2:
            combined_name = self.first_name1 + ' & ' + self.first_name2 + ' ' + str(self.last_name1)
    elif self.last_name1:  # Only last_name1 is set
        pass
    elif self.last_name2:  # Only last_name2 is set
        pass
    else: # Both last_name1 and last_name2 are None or ''
        pass

    return combined_name

Upvotes: 1

thebjorn
thebjorn

Reputation: 27321

The way you've defined the names, any and all of them can be None, if you change them to the empty string you'll have similar problems. To illustrate, lets start by writing a unit test (substitute the empty string for None if you want):

def test_contact_full_name():
    # correct.
    assert Contact('Jane', None, 'John', None).full_name == "Jane & John"
    assert Contact('Bart', 'Simpson', 'Lisa', 'Simpson').full_name  == "Bart & Lisa Simpson"
    assert Contact('Bart', 'Simpson', 'Frodo', 'Baggins').full_name == "Bart Simpson & Frodo Baggins"
    assert Contact('Bart', None, None, None).full_name == "Bart"
    assert Contact('Bart', 'Simpson', None, None).full_name == "Bart Simpson"
    assert Contact(None, 'Simpson', None, None).full_name == "Simpson"

    assert Contact(None, None, None, None).full_name == ""

    # correct?
    assert Contact('Bart', 'Simpson', 'Lisa', None).full_name == "Bart Simpson & Lisa"

    # correct??
    assert Contact('Bart', None, 'Lisa', 'Simpson').full_name == "Bart & Lisa Simpson"

Then it's just a question of dividing the problem into smaller pieces, I've put everything into a regular class just to make it easier to test. First some helper methods:

class Contact(object):
    def __init__(self, a, b, c, d):
        self.first_name1 = a
        self.last_name1 = b
        self.first_name2 = c
        self.last_name2 = d

    def combined_last_name(self, a, b):
        "Return ``a`` if ``a`` and ``b`` are equal, otherwise returns None."
        return a if a and b and a == b else None

    def normalize_name(self, n):
        "Returns the name or the empty string if ``n`` is None."
        return n if n else ""

    def get_name(self, f, l):
        """Returns a string containing firstname lastname and omits any of them
           if they're None.
        """         
        if f and l:
            return "%s %s" % (f, l)
        if f:
            return f
        elif l:
            return l
        return ""

    def has_second_name(self):
        "Returns true if there is a second name."
        return self.first_name2 or self.last_name2

then we can define the full_name property:

    @property
    def full_name(self):
        """Returns a string that combines first and last names of two people
           depending if the last names are the same or not. If the last name
           is the same, it displays as::

               first_name1 & first_name2 last_name1

        """
        cln = self.combined_last_name(self.last_name1, self.last_name2)
        if cln:  # have a common last name..
            return "%s & %s %s" % (
                self.first_name1,
                self.first_name2,
                cln
            )
        elif self.has_second_name():
            return "%s & %s" % (
                self.get_name(self.first_name1, self.last_name1),
                self.get_name(self.first_name2, self.last_name2)
            )
        else:
            return self.get_name(self.first_name1, self.last_name1)

if we put everything in a file named fullname.py we can use the pytest tool (pip install pytest) to run the tests:

c:\srv\tmp> pytest --verbose fullname.py
============================= test session starts =============================
platform win32 -- Python 2.7.16, pytest-3.3.1, py-1.5.2, pluggy-0.6.0 -- c:\srv\venv\finautfaktura\scripts\python.exe
cachedir: .cache
rootdir: c:\srv\tmp, inifile:
plugins: xdist-1.20.1, forked-0.2, django-3.1.2, cov-2.5.1
collected 1 item

fullname.py::test_contact_full_name PASSED                               [100%]

========================== 1 passed in 0.20 seconds ===========================

All is well... or is it?

Let's write another test:

def test_only_second_name():
    assert Contact(None, None, None, "Simpson").full_name == "Simpson"
    assert Contact(None, None, "Lisa", "Simpson").full_name == "Lisa Simpson"
    assert Contact(None, None, "Lisa", None).full_name == "Lisa"

running pytest again reveals the (first) error:

c:\srv\tmp> pytest --verbose fullname.py
============================= test session starts =============================
platform win32 -- Python 2.7.16, pytest-3.3.1, py-1.5.2, pluggy-0.6.0 -- c:\srv\venv\finautfaktura\scripts\python.exe
cachedir: .cache
rootdir: c:\srv\tmp, inifile:
plugins: xdist-1.20.1, forked-0.2, django-3.1.2, cov-2.5.1
collected 2 items

fullname.py::test_contact_full_name PASSED                               [ 50%]
fullname.py::test_only_second_name FAILED                                [100%]

================================== FAILURES ===================================
____________________________ test_only_second_name ____________________________

    def test_only_second_name():
>       assert Contact(None, None, None, "Simpson").full_name == "Simpson"
E       AssertionError: assert ' & Simpson' == 'Simpson'
E         -  & Simpson
E         ? ---
E         + Simpson

fullname.py:83: AssertionError
===================== 1 failed, 1 passed in 0.37 seconds ======================

i.e. the property returned " & Simpson" instead of the expected "Simpson" for the first assert.

To fix this we can make the full_name property handle this added complexity as well, or.., we can solve the problem somewhere else, e.g. in the __init__:

class Contact(object):
    def __init__(self, a, b, c, d):
        self.first_name1 = a
        self.last_name1 = b
        self.first_name2 = c
        self.last_name2 = d
        if not a and not b:
            # if no name1, then put name2 into name1 and set name2 to None
            self.first_name1 = self.first_name2
            self.last_name1 = self.last_name2
            self.first_name2 = self.last_name2 = None

running pytest again shows that this fixed the second test.

You can of course not provide your own __init__ in a Django model to solve this problem, but you can do something similar in if you override the save(..) method:

def save(self, *args, **kwargs):
    if not self.first_name1 and not self.last_name1:
        self.first_name1 = self.first_name2
        self.last_name1 = self.last_name2
        self.first_name2 = self.last_name2 = None
    super(Contact, self).save(*args, **kwargs)

Upvotes: 0

Paolo
Paolo

Reputation: 853

The reason on why you're getting an error that last_name1 must be a String, not a NoneType is due to the fact you've set null to True in your field declaration for the said field.

So what's wrong with doing that? When you are defining null=True for fields like CharField or TextField you'll end up having None. The Django convention is to use EMPTY STRING.

Here's a link that talks about how you would use blank or null on field declarations.

Upvotes: 1

Related Questions