Git Product home page Git Product logo

django-natural-keys's Introduction

Django Natural Keys

Enhanced support for natural keys in Django and Django REST Framework. Extracted from wq.db for general use.

Django Natural Keys provides a number of useful model methods (e.g. get_or_create_by_natural_key()) that speed up working with natural keys in Django. The module also provides a couple of serializer classes that streamline creating REST API support for models with natural keys.

Latest PyPI Release Release Notes License GitHub Stars GitHub Forks GitHub Issues

Tests Python Support Django Support

Usage

Django Natural Keys is available via PyPI:

# Recommended: create virtual environment
# python3 -m venv venv
# . venv/bin/activate
pip install natural-keys

Model API

To use natural keys in vanilla Django, you need to define a natural_key() method on your Model class and a get_natural_key() method on the Manager class. With Django Natural Keys, you can instead extend NaturalKeyModel and define one of the following:

The first unique constraint found will be treated as the natural key for the model, and all of the necessary functions for working with natural keys will automatically work.

from natural_keys import NaturalKeyModel

class Event(NaturalKeyModel):
    name = models.CharField(max_length=255)
    date = models.DateField()
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=('name', 'date'),
                name='event_natural_key',
            )
        ]
        
class Note(models.Model):
    event = models.ForeignKey(Event)
    note = models.TextField()

or

from natural_keys import NaturalKeyModel

class Event(NaturalKeyModel):
    name = models.CharField(unique=True)

The following methods will then be available on your Model and its Manager:

# Default Django methods
instance = Event.objects.get_by_natural_key('ABC123', date(2016, 1, 1))
instance.natural_key == ('ABC123', date(2016, 1, 1))

# get_or_create + natural keys
instance, is_new = Event.objects.get_or_create_by_natural_key('ABC123', date(2016, 1, 1))

# Like get_or_create_by_natural_key, but discards is_new
# Useful for quick lookup/creation when you don't care whether the object exists already
instance = Event.objects.find('ABC123', date(2016, 1, 1))
note = Note.objects.create(
     event=Event.objects.find('ABC123', date(2016, 1, 1)),
     note="This is a note"
)
instance == note.event

# Inspect natural key fields on a model without instantiating it
Event.get_natural_key_fields() == ('name', 'date')

Nested Natural Keys

One key feature of Django Natural Keys is that it will automatically traverse ForeignKeys to related models (which should also be NaturalKeyModel classes). This makes it possible to define complex, arbitrarily nested natural keys with minimal effort.

class Place(NaturalKeyModel):
    name = models.CharField(max_length=255, unique=True)

class Event(NaturalKeyModel):
    place = models.ForeignKey(Place)
    date = models.DateField()
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=('place', 'date'),
                name='event_natural_key',
            )
        ]
Event.get_natural_key_fields() == ('place__name', 'date')
instance = Event.find('ABC123', date(2016, 1, 1))
instance.place.name == 'ABC123'

REST Framework Support

Django Natural Keys provides several integrations with Django REST Framework, primarily through custom Serializer classes. In most cases, you will want to use either:

  • NaturalKeyModelSerializer, or
  • The natural_key_slug pseudo-field (see below)

If you have only a single model with a single char field for its natural key, you probably do not need to use either of these integrations. In your view, you can just use Django REST Framework's built in lookup_field to point directly to your natural key.

NaturalKeyModelSerializer

NaturalKeyModelSerializer facilitates handling complex natural keys in your rest API. It can be used with a NaturalKeyModel, or (more commonly) a model that has a foreign key to a NaturalKeyModel but is not a NaturalKeyModel itself. (One concrete example of this is the vera.Report model, which has a ForeignKey to vera.Event, which is a NaturalKeyModel).

NaturalKeyModelSerializer extends DRF's ModelSerializer, but uses NaturalKeySerializer for each foreign key that points to a NaturalKeyModel. When update() or create()ing the primary model, the nested NaturalKeySerializers will automatically create instances of referenced models if they do not exist already (via the find() method described above). Note that NaturalKeyModelSerializer does not override DRF's default behavior for other fields, whether or not they form part of the primary model's natural key.

NaturalKeySerializer can technically be used as a top level serializer, though this is not recommended. NaturalKeySerializer is designed for dealing with nested natural keys and does not support updates or non-natural key fields. Even when used together with NaturalKeyModelSerializer, NaturalKeySerializer never updates an existing related model instance. Instead, it will repoint the foreign key to another (potentially new) instance of the related model. It may help to think of NaturalKeySerializer as a special RelatedField class rather than as a Serializer per se.

You can use NaturalKeyModelSerializer with Django REST Framework and/or wq.db just like any other serializer:

# Django REST Framework usage example
from rest_framework import viewsets
from rest_framework import routers
from natural_keys import NaturalKeyModelSerializer
from .models import Event, Note

class EventSerializer(NaturalKeyModelSerializer):
    class Meta:
        model = Event
        
class NoteSerializer(NaturalKeyModelSerializer):
    class Meta:
        model = Note

class EventViewSet(viewsets.ModelViewSet):
    queryset = Event.objects.all()
    serializer_class = EventSerializer

class NoteViewSet(viewsets.ModelViewSet):
    queryset = Note.objects.all()
    serializer_class = NoteSerializer

router = routers.DefaultRouter()
router.register(r'events', EventViewSet)
router.register(r'notes', NoteViewSet)

# wq.db usage example
from wq.db import rest
from natural_keys import NaturalKeyModelSerializer
from .models import Event, Note

rest.router.register_model(Note, serializer=NaturalKeyModelSerializer)
rest.router.register_model(Event, serializer=NaturalKeyModelSerializer)

Once this is set up, you can use your REST API to create and view your NaturalKeyModel instances and related data. To facilitate integration with regular HTML Forms, Django Natural Keys is integrated with the HTML JSON Forms package, which supports nested keys via an array naming convention, as the examples below demonstrate.

<form action="/events/" method="post">
  <input name="place[name]">
  <input type="date" name="date">
</form>
// /events.json
[
    {
        "id": 123,
        "place": {"name": "ABC123"},
        "date": "2016-01-01"
    }
]
<form action="/notes/" method="post">
  <input name="event[place][name]">
  <input type="date" name="event[date]">
  <textarea name="note"></textarea>
</form>
// /notes.json
[
    {
        "id": 12345,
        "event": {
            "place": {"name": "ABC123"},
            "date": "2016-01-01"
        },
        "note": "This is a note"
    }
]

Natural Key Slugs

As an alternative to using NaturalKeyModelSerializer / NaturalKeySerializer, you can also use a single slug-like field for lookup and serialization. NaturalKeyModel (and its associated queryset) defines a pseudo-field, natural_key_slug, for this purpose.

class Place(NaturalKeyModel):
    name = models.CharField(max_length=255, unique=True)
    
class Room(NaturalKeyModel)
    place = models.ForeignKey(Place, models.ON_DELETE)
    name = models.CharField(max_length=255)
    
    class Meta:
        unique_together = (('place', 'name'),)
room = Room.objects.find("ABC123", "MainHall")
assert(room.natural_key_slug == "ABC123-MainHall")
assert(room == Room.objects.get(natural_key_slug="ABC123-MainHall"))

You can expose this functionality in your REST API to expose natural keys instead of database-generated ids. To do this, you will likely want to do the following:

  1. Create a regular serializer with id = serializers.ReadOnlyField(source='natural_key_slug')
  2. Set lookup_field = 'natural_key_slug' on your ModelViewSet (or similar generic class) and update the URL registration accordingly
  3. Ensure foreign keys on any related models are serialized with serializers.SlugRelatedField(slug_field='natural_key_slug')

In wq.db, all three of the above can be achieved by setting the "lookup" attribute when registering with the router:

# myapp/rest.py
from wq.db import rest
from .models import Room

rest.router.register_model(
    Room,
    fields='__all__',
    lookup='natural_key_slug',
)

Note that the natural_key_slug may not behave as expected if any of the component values contain the delimiter character (- by default). To mitigate this, you can set natural_key_separator on the model class to another character.

django-natural-keys's People

Contributors

achembarpu avatar marcosox avatar ofalk avatar realmhamdy avatar sheppard avatar tomaszn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

django-natural-keys's Issues

Use as a decorator

Requiring that a model inherit from NaturalKeyModel can conflict with other model modifiers that also requiring inheritance. It would be nice if the same functionality could be exposed via a class decorator. E.g.,

@NaturallyKeyedModel
class MyModel(SomeOtherModelSuperclass):
    # ...

AttributeError: 'CharField' object has no attribute 'rel'

Hi!

Today I upgraded my dev instance to Django 2.0 and after (of course) fixing a few things, I'm having a problem with django-natural-keys.
Exactly:

rel_to = field.rel.to if field.rel else None

For whatever reason CharObject (and probably others as well) do no longer provide the attribute 'rel'.

I've changed the code for the moment, that it looks this way:

rel_to = None
if hasattr(field, 'rel'):
    rel_to = field.rel.to if field.rel else None

But I'm not 100 % confident, that this is the (safe) way to go.

See also: AttributeError: model object has no attribute 'rel' @ Stackoverflow

Thx for

unique_together may be deprecated in the future - Consider Unique Constraints?

Django 2.2 introduces Unique Constraint meta option which provides greater flexibility in defining uniqueness constrains. Currently django-natural-keys does not consider Unique Constraints. According to latest (3.0) Django documentation for unique_together meta option:

Use UniqueConstraint with the constraints option instead.

UniqueConstraint provides more functionality than unique_together. unique_together may be deprecated in the future.

Therefore NaturalKeyModel should be updated to check for Unique Constraints to define natural key fields

Problem with null refs

Hi, I had a problem importing FK:s with null value. I added this code below to my class to work around.
I have a feeling though that this is not entierly correct if the Null appears on a different level??
(Ex: It fails when re-importing an existing record, calls constructor with field_id-values but constructor looks for field. Seems like constructor should never have been called.)

    def natural_key(self):
        "Override implementation to handle null refs"
        # Recursively extract properties from related objects if needed
        vals = [reduce(getattr, name_list, self) if getattr(self, name_list[0]) is not None else None
                for name_list in [name.split('__') for name in self.get_natural_key_fields()]]
        return vals

Orig impl in models.py line 215:

    def natural_key(self):
        """
        Return the natural key for this object.

        (This is a generic implementation of the standard Django function)
        """
        # Recursively extract properties from related objects if needed
        vals = [reduce(getattr, name.split('__'), self)       # Will call reduce on None
                for name in self.get_natural_key_fields()]
        return vals

Cannot run update on a non-natural field?

I am getting the following field when trying to run this code:
"NotImplementedError: Updating an existing natural key is not supported."

However, I am not trying to update the natural key.

Below is my model:
class User(BaseModel):
"""Stores the information associated with a User
first_name - The user's first name.
middle_name - The user's middle name.
last_name - The user's last name.
title - The user's official title.
eauth_id - The user's FSIS eAuth ID.
email - The user's email.
desk_phone - The user's office phone.
desk_phone_extension - The user's office phone extension.
mobile_phone - The user's personal cell phone.
home_phone - The user's home phone.
fsis_cell_phone - The user's FSIS cell phone.
status - The user's activity status.
last_login_time - The timestamp for the last successful login.
access_level - Foreign Key to ListItem Access Level.
sec_user - Foreign Key to security User.
"""

first_name = models.CharField(max_length=100)
middle_name = models.CharField(max_length=100, null=True, blank=True)
last_name = models.CharField(max_length=100)
title = models.CharField(max_length=100, null=True, blank=True)
eauth_id = models.CharField(max_length=255, unique=True)
email = models.EmailField(max_length=255)
desk_phone = models.CharField(max_length=12, null=True, blank=True)
desk_phone_extension = models.CharField(
    max_length=12, null=True, blank=True)
mobile_phone = models.CharField(max_length=12, null=True, blank=True)
home_phone = models.CharField(max_length=12, null=True, blank=True)
fsis_cell_phone = models.CharField(max_length=12, null=True, blank=True)
last_login_time = models.DateTimeField(
    null=True, blank=True)
status = models.ForeignKey(
    ListItem, related_name='user_status', on_delete=models.CASCADE)
access_level = models.ForeignKey(
    ListItem, related_name='access_level', on_delete=models.CASCADE)
sec_user = models.ForeignKey(
    SecUser, related_name='frio_user', on_delete=models.CASCADE)

def __str__(self):
    return '%s %s - %s' % (self.first_name, self.last_name, self.email)</code>

Here is my serializer:
class UserSerializer(NaturalKeySerializer):
"""Validates and serializes the User model.

Fields:
* id
* url
* created_time
* last_updated
* eauth_id
* first_name
* middle_name
* last_name
* title
* email
* desk_phone
* desk_phone_extension
* mobile_phone
* fsis_cell_phone
* status_id
* home_phone
* status_name
* status_display_text
* last_login_time
* access_level_id
* access_level_name
* access_level_display_text

"""
# The phone group requires this not_required_char to work.
not_required = {'allow_null': True, 'required': False}
not_required_char = not_required.copy()
not_required_char.update({'allow_blank': True})
phone_regex = r"^\(?[2-9]\d\d\)?-\d{3}-\d{4}$"
desk_phone = serializers.RegexField(regex=phone_regex, **not_required_char)
mobile_phone = serializers.RegexField(
    regex=phone_regex, **not_required_char)
home_phone = serializers.RegexField(regex=phone_regex, **not_required_char)
fsis_cell_phone = serializers.RegexField(
    regex=phone_regex, **not_required_char)
status_id = serializers.IntegerField(required=False)
status_name = StatusLookupField()
status_display_text = serializers.CharField(
    source='status.display_text', read_only=True)
access_level_id = serializers.IntegerField(required=False)
access_level_name = AccessLevelField(required=True)
access_level_display_text = serializers.CharField(
    source='access_level.display_text', read_only=True, required=False)
sec_user = serializers.PrimaryKeyRelatedField(read_only=True)

def validate(self, data):
    phone_fields = (
        'desk_phone',
        'mobile_phone',
        'home_phone',
        'fsis_cell_phone',
    )

    if self.instance:
        new_data = model_to_dict(self.instance)
    else:
        new_data = {}
    new_data.update(data)

    if all((new_data.get(f, '') == '' or new_data.get(f, '') is None)
            for f in phone_fields):
        raise serializers.ValidationError(
            'At least one phone is required.')

    if not (new_data.get('status') or new_data.get('status_id')):
        raise serializers.ValidationError(
            'status_id or status_name is required.')

    return data

def create(self, validated_data):
    self._link_sec_user(validated_data)
    return super(UserSerializer, self).create(validated_data)

def _link_sec_user(self, validated_data):
    """Gets or creates a SecUser with the provided eauth_id and sets it on
    the FRIO user's sec_user field."""
    eauth_id = validated_data.get('eauth_id')
    if eauth_id:
        module = USDAModule.objects.get(name='FRIO')
        sec_user, created = SecUser.objects.get_or_create(
            module=module, user_name=eauth_id)
        validated_data['sec_user'] = sec_user

def update(self, instance, validated_data):
    self._update_sec_user(instance, validated_data)
    return super(UserSerializer, self).update(instance, validated_data)

def _update_sec_user(self, instance, validated_data):
    """Updates the security user's user_name to match the eauth_id."""
    eauth_id = validated_data.get('eauth_id')
    if eauth_id and instance.sec_user.user_name != eauth_id:
        instance.sec_user.user_name = eauth_id
        instance.sec_user.save()

class Meta:
    model = models.User
    fields = ('id', 'url', 'created_time', 'last_updated',
              'eauth_id', 'first_name', 'middle_name', 'last_name',
              'title', 'email', 'desk_phone', 'desk_phone_extension',
              'mobile_phone', 'home_phone', 'fsis_cell_phone', 'status_id',
              'status_name', 'status_display_text',
              'last_login_time', 'access_level_id', 'access_level_name',
              'access_level_display_text',
              'sec_user'
              )</code>

And the code that I am trying to run that is breaking is:
user = client.frio.users.update(user['id'], {'last_login_time': cur_time})

As you can see, I am trying to update 'last_login_time' and the natural_key on that model is eauth_id. Looking at the code it seems that update is just not implemented in your code. Am I missing something?

AttributeError: 'Q' object has no attribute 'split'

Hi. With 1.4.0 I'm getting following error:

AttributeError: 'Q' object has no attribute 'split'

Traceback:

  File "/Users/dexter/src/gui/project/admin.py", line 179, in changelist_view
    return super().changelist_view(request, extra_context)
  File "/Users/dexter/src/gui/.virtualenv/lib/python3.6/site-packages/django/utils/decorators.py", line 67, in _wrapper
    return bound_func(*args, **kwargs)
  File "/Users/dexter/src/gui/.virtualenv/lib/python3.6/site-packages/django/utils/decorators.py", line 149, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "/Users/dexter/src/gui/.virtualenv/lib/python3.6/site-packages/django/utils/decorators.py", line 63, in bound_func
    return func.__get__(self, type(self))(*args2, **kwargs2)
  File "/Users/dexter/src/gui/.virtualenv/lib/python3.6/site-packages/django/contrib/admin/options.py", line 1542, in changelist_view
    self.list_max_show_all, self.list_editable, self,
  File "/Users/dexter/src/gui/.virtualenv/lib/python3.6/site-packages/django/contrib/admin/views/main.py", line 78, in __init__
    self.queryset = self.get_queryset(request)
  File "/Users/dexter/src/gui/.virtualenv/lib/python3.6/site-packages/django/contrib/admin/views/main.py", line 351, in get_queryset
    qs, search_use_distinct = self.model_admin.get_search_results(request, qs, self.query)
  File "/Users/dexter/src/gui/.virtualenv/lib/python3.6/site-packages/django/contrib/admin/options.py", line 910, in get_search_results
    queryset = queryset.filter(reduce(operator.or_, or_queries))
  File "/Users/dexter/src/gui/.virtualenv/lib/python3.6/site-packages/natural_keys/models.py", line 8, in filter
    slugs = natural_key_slug.split(
AttributeError: 'Q' object has no attribute 'split'
>>> natural_key_slug
<Q: (OR: ('id__iexact', '9'), ('name__istartswith', '9'))>

It was after I tried to use "Search" field in list view.

Is rest_framework forgotten in django-natural-keys ?

Hello,

I'm discovering django-natural-keys.
It seems djangorestframework is required for loaddata/dumpdata operations.

Does it make sense to add djangorestframework as django-natural-keys dependencies ?

Best regards

Feature request: Model __repr__

I often user natural key in my models' repr, so adding this here would be nice, something like:

'<%s: %s>' % (self._meta.model.__name__, ', '.join(self.natural_key()))

RFE: representation of datetime

The str() representation of a datetime value is s.isoformat(sep=' ') whereas the representation of a DateTimeField is s.isoformat(sep='T').

It would be nice to have the option for natural_key representation of datetime values using the latter... perhaps adding a datetime_separator variable similar to the natural_key_separator variable. Example attached.

thanks

natural_keys.models.py.txt

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.