Compruebe si OneToOneField es Ninguno en Django

86

Tengo dos modelos como este:

class Type1Profile(models.Model):
    user = models.OneToOneField(User, unique=True)
    ...


class Type2Profile(models.Model):
    user = models.OneToOneField(User, unique=True)
    ...

Necesito hacer algo si el usuario tiene un perfil Type1 o Type2:

if request.user.type1profile != None:
    # do something
elif request.user.type2profile != None:
    # do something else
else:
    # do something else

Pero, para los usuarios que no tienen perfiles type1 o type2, ejecutar un código como ese produce el siguiente error:

Type1Profile matching query does not exist.

¿Cómo puedo comprobar el tipo de perfil que tiene un usuario?

Gracias

John Bright
fuente

Respuestas:

93

Para verificar si la relación (OneToOne) existe o no, puede usar la hasattrfunción:

if hasattr(request.user, 'type1profile'):
    # do something
elif hasattr(request.user, 'type2profile'):
    # do something else
else:
    # do something else
joctee
fuente
4
Gracias por esta solución Desafortunadamente, esto no funciona todo el tiempo. En caso de que desee trabajar con select_related()ahora o en el futuro, o tal vez incluso para estar seguro de que también maneja otros tipos de magia que pueden suceder en otros lugares, debe extender la prueba de la siguiente manera:if hasattr(object, 'onetoonerevrelattr') and object.onetoonerevrelattr != None
class stacker
7
Tenga en cuenta que en Python <3.2, hasattrse tragarán todas las excepciones que sucedan durante la búsqueda de la base de datos, y no solo DoesNotExist. Probablemente esté roto, y no es lo que quieres.
Pi Delport
no funciona con python 2.7. Incluso si OneToOne no existe, devuelve un objeto django.db.models.fields.related.RelatedManager.
alexpirina
@alartur ¿qué versión de django estás usando?
joctee
Django 1.5. Pero resolví mi problema particular implementando lo que quería hacer de una manera completamente diferente.
Alexpirina
48

Es posible ver si una relación uno a uno que acepta valores NULL es nula para un modelo en particular simplemente probando el campo correspondiente en el modelo None, pero solo si prueba en el modelo donde se origina la relación uno a uno. Por ejemplo, dadas estas dos clases ...

class Place(models.Model):
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=80)

class Restaurant(models.Model):  # The class where the one-to-one originates
    place = models.OneToOneField(Place, blank=True, null=True)
    serves_hot_dogs = models.BooleanField()
    serves_pizza = models.BooleanField()

... para ver si a Restauranttiene un Place, podemos usar el siguiente código:

>>> r = Restaurant(serves_hot_dogs=True, serves_pizza=False)
>>> r.save()
>>> if r.place is None:
>>>    print "Restaurant has no place!"
Restaurant has no place!

Para ver si a Placetiene un Restaurant, es importante comprender que hacer referencia a la restaurantpropiedad en una instancia de Placegenera una Restaurant.DoesNotExistexcepción si no hay un restaurante correspondiente. Esto sucede porque Django realiza una búsqueda internamente usando QuerySet.get(). Por ejemplo:

>>> p2 = Place(name='Ace Hardware', address='1013 N. Ashland')
>>> p2.save()
>>> p2.restaurant
Traceback (most recent call last):
    ...
DoesNotExist: Restaurant matching query does not exist.

En este escenario, prevalece la navaja de Occam, y el mejor enfoque para tomar una determinación sobre si un Placetiene o no Restautrantsería un estándar try/ exceptconstrucción como se describe aquí .

>>> try:
>>>     restaurant = p2.restaurant
>>> except Restaurant.DoesNotExist:
>>>     print "Place has no restaurant!"
>>> else:
>>>     # Do something with p2's restaurant here.

Si bien la sugerencia de joctee de usar hasattrfunciona en la práctica, en realidad solo funciona por accidente, ya que hasattrsuprime todas las excepciones (incluidas DoesNotExist) en lugar de solo AttributeErrors, como debería. Como señaló Pi Delport , este comportamiento en realidad se corrigió en Python 3.2 según el siguiente ticket: http://bugs.python.org/issue9666 . Además, y a riesgo de parecer obstinado, creo que la construcción try/ anterior exceptes más representativa de cómo funciona Django, mientras que el uso hasattrpuede nublar el problema para los principiantes, lo que puede crear FUD y difundir malos hábitos.

EDITAR El compromiso razonable de Don Kirkby también me parece razonable.

Joshua Pokotilow
fuente
19

Me gusta la respuesta de joctee , porque es muy simple.

if hasattr(request.user, 'type1profile'):
    # do something
elif hasattr(request.user, 'type2profile'):
    # do something else
else:
    # do something else

Otros comentaristas han expresado su preocupación de que es posible que no funcione con ciertas versiones de Python o Django, pero la documentación de Django muestra esta técnica como una de las opciones:

También puede usar hasattr para evitar la necesidad de capturar excepciones:

>>> hasattr(p2, 'restaurant')
False

Por supuesto, la documentación también muestra la técnica de captura de excepciones:

p2 no tiene un restaurante asociado:

>>> from django.core.exceptions import ObjectDoesNotExist
>>> try:
>>>     p2.restaurant
>>> except ObjectDoesNotExist:
>>>     print("There is no restaurant here.")
There is no restaurant here.

Estoy de acuerdo con Joshua en que detectar la excepción aclara lo que está sucediendo, pero me parece más complicado. ¿Quizás este sea un compromiso razonable?

>>> print(Restaurant.objects.filter(place=p2).first())
None

Esto es solo consultar los Restaurantobjetos por lugar. Vuelve Nonesi ese lugar no tiene restaurante.

Aquí hay un fragmento ejecutable para que juegues con las opciones. Si tiene Python, Django y SQLite3 instalados, simplemente debería ejecutarse. Lo probé con Python 2.7, Python 3.4, Django 1.9.2 y SQLite3 3.8.2.

# Tested with Django 1.9.2
import sys

import django
from django.apps import apps
from django.apps.config import AppConfig
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db import connections, models, DEFAULT_DB_ALIAS
from django.db.models.base import ModelBase

NAME = 'udjango'


def main():
    setup()

    class Place(models.Model):
        name = models.CharField(max_length=50)
        address = models.CharField(max_length=80)

        def __str__(self):              # __unicode__ on Python 2
            return "%s the place" % self.name

    class Restaurant(models.Model):
        place = models.OneToOneField(Place, primary_key=True)
        serves_hot_dogs = models.BooleanField(default=False)
        serves_pizza = models.BooleanField(default=False)

        def __str__(self):              # __unicode__ on Python 2
            return "%s the restaurant" % self.place.name

    class Waiter(models.Model):
        restaurant = models.ForeignKey(Restaurant)
        name = models.CharField(max_length=50)

        def __str__(self):              # __unicode__ on Python 2
            return "%s the waiter at %s" % (self.name, self.restaurant)

    syncdb(Place)
    syncdb(Restaurant)
    syncdb(Waiter)

    p1 = Place(name='Demon Dogs', address='944 W. Fullerton')
    p1.save()
    p2 = Place(name='Ace Hardware', address='1013 N. Ashland')
    p2.save()
    r = Restaurant(place=p1, serves_hot_dogs=True, serves_pizza=False)
    r.save()

    print(r.place)
    print(p1.restaurant)

    # Option 1: try/except
    try:
        print(p2.restaurant)
    except ObjectDoesNotExist:
        print("There is no restaurant here.")

    # Option 2: getattr and hasattr
    print(getattr(p2, 'restaurant', 'There is no restaurant attribute.'))
    if hasattr(p2, 'restaurant'):
        print('Restaurant found by hasattr().')
    else:
        print('Restaurant not found by hasattr().')

    # Option 3: a query
    print(Restaurant.objects.filter(place=p2).first())


def setup():
    DB_FILE = NAME + '.db'
    with open(DB_FILE, 'w'):
        pass  # wipe the database
    settings.configure(
        DEBUG=True,
        DATABASES={
            DEFAULT_DB_ALIAS: {
                'ENGINE': 'django.db.backends.sqlite3',
                'NAME': DB_FILE}},
        LOGGING={'version': 1,
                 'disable_existing_loggers': False,
                 'formatters': {
                    'debug': {
                        'format': '%(asctime)s[%(levelname)s]'
                                  '%(name)s.%(funcName)s(): %(message)s',
                        'datefmt': '%Y-%m-%d %H:%M:%S'}},
                 'handlers': {
                    'console': {
                        'level': 'DEBUG',
                        'class': 'logging.StreamHandler',
                        'formatter': 'debug'}},
                 'root': {
                    'handlers': ['console'],
                    'level': 'WARN'},
                 'loggers': {
                    "django.db": {"level": "WARN"}}})
    app_config = AppConfig(NAME, sys.modules['__main__'])
    apps.populate([app_config])
    django.setup()
    original_new_func = ModelBase.__new__

    @staticmethod
    def patched_new(cls, name, bases, attrs):
        if 'Meta' not in attrs:
            class Meta:
                app_label = NAME
            attrs['Meta'] = Meta
        return original_new_func(cls, name, bases, attrs)
    ModelBase.__new__ = patched_new


def syncdb(model):
    """ Standard syncdb expects models to be in reliable locations.

    Based on https://github.com/django/django/blob/1.9.3
    /django/core/management/commands/migrate.py#L285
    """
    connection = connections[DEFAULT_DB_ALIAS]
    with connection.schema_editor() as editor:
        editor.create_model(model)

main()
Don Kirkby
fuente
10

¿Qué tal usar bloques try / except?

def get_profile_or_none(user, profile_cls):

    try:
        profile = getattr(user, profile_cls.__name__.lower())
    except profile_cls.DoesNotExist:
        profile = None

    return profile

¡Entonces, úsala así!

u = request.user
if get_profile_or_none(u, Type1Profile) is not None:
    # do something
elif get_profile_or_none(u, Type2Profile) is not None:
    # do something else
else:
    # d'oh!

Supongo que podría usar esto como una función genérica para obtener cualquier instancia inversa de OneToOne, dada una clase de origen (aquí: sus clases de perfil) y una instancia relacionada (aquí: request.user).

Geradeausanwalt
fuente
3

Utilice select_related!

>>> user = User.objects.select_related('type1profile').get(pk=111)
>>> user.type1profile
None
ivan133
fuente
2
Sé que funciona así, pero ¿está realmente documentado este comportamiento de select_related?
Kos
3
Acabo de probar esto en Django 1.9.2 y surge RelatedObjectDoesNotExist.
Don Kirkby
1

en caso de que tengas el modelo

class UserProfile(models.Model):
    user = models.OneToOneField(User, unique=True)

Y solo necesita saber para cualquier usuario que UserProfile existe / o no, la forma más eficiente desde el punto de vista de la base de datos para usar la consulta existente .

La consulta existente devolverá solo un booleano, en lugar de un acceso de atributo inverso como hasattr(request.user, 'type1profile'), lo que generará una consulta de obtención y devolverá la representación completa del objeto

Para hacerlo, debe agregar una propiedad al modelo de usuario

class User(AbstractBaseUser)

@property
def has_profile():
    return UserProfile.objects.filter(user=self.pk).exists()
pymen
fuente
0

Estoy usando una combinación de has_attr y es None:

class DriverLocation(models.Model):
    driver = models.OneToOneField(Driver, related_name='location', on_delete=models.CASCADE)

class Driver(models.Model):
    pass

    @property
    def has_location(self):
        return not hasattr(self, "location") or self.location is None
FreeWorlder
fuente
0

Uno de los enfoques inteligentes será agregar el campo personalizado OneToOneOrNoneField y usarlo [funciona para Django> = 1.9]

from django.db.models.fields.related_descriptors import ReverseOneToOneDescriptor
from django.core.exceptions import ObjectDoesNotExist
from django.db import models


class SingleRelatedObjectDescriptorReturnsNone(ReverseOneToOneDescriptor):
    def __get__(self, *args, **kwargs):
        try:
            return super().__get__(*args, **kwargs)
        except ObjectDoesNotExist:
            return None


class OneToOneOrNoneField(models.OneToOneField):
    """A OneToOneField that returns None if the related object doesn't exist"""
    related_accessor_class = SingleRelatedObjectDescriptorReturnsNone

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('null', True)
        kwargs.setdefault('blank', True)
        super().__init__(*args, **kwargs)

Implementación

class Restaurant(models.Model):  # The class where the one-to-one originates
    place = OneToOneOrNoneField(Place)
    serves_hot_dogs = models.BooleanField()
    serves_pizza = models.BooleanField()

Uso

r = Restaurant(serves_hot_dogs=True, serves_pizza=False)
r.place  # will return None
pymen
fuente
para django 1.8 necesita usar en SingleRelatedObjectDescriptorlugar de ReverseOneToOneDescriptoresto from django.db.models.fields.related import SingleRelatedObjectDescriptor
pymen