Package: natural-keys
Django Natural KeysEnhanced support fornatural keysin Django andDjango REST Framework.  Extracted fromwq.dbfor general use.Django Natural Keysprovides 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.UsageDjango Natural Keysis available via PyPI:# Recommended: create virtual environment# python3 -m venv venv# . venv/bin/activatepipinstallnatural-keysModel APITo usenatural keysin vanilla Django, you need to define anatural_key()method on your Model class and aget_natural_key()method on the Manager class.  WithDjango Natural Keys, you can instead extendNaturalKeyModeland define one of the following:AUniqueConstraintinMeta.constraints(recommended),A tuple inMeta.unique_together, orAmodel field(other thanAutoField) withunique=TrueThe 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.fromnatural_keysimportNaturalKeyModelclassEvent(NaturalKeyModel):name=models.CharField(max_length=255)date=models.DateField()classMeta:constraints=[models.UniqueConstraint(fields=('name','date'),name='event_natural_key',)]classNote(models.Model):event=models.ForeignKey(Event)note=models.TextField()orfromnatural_keysimportNaturalKeyModelclassEvent(NaturalKeyModel):name=models.CharField(unique=True)The following methods will then be available on your Model and its Manager:# Default Django methodsinstance=Event.objects.get_by_natural_key('ABC123',date(2016,1,1))instance.natural_key==('ABC123',date(2016,1,1))# get_or_create + natural keysinstance,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 alreadyinstance=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 itEvent.get_natural_key_fields()==('name','date')Nested Natural KeysOne key feature ofDjango Natural Keysis that it will automatically traverseForeignKeys to related models (which should also beNaturalKeyModelclasses).  This makes it possible to define complex, arbitrarily nested natural keys with minimal effort.classPlace(NaturalKeyModel):name=models.CharField(max_length=255,unique=True)classEvent(NaturalKeyModel):place=models.ForeignKey(Place)date=models.DateField()classMeta: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 SupportDjango Natural Keysprovides several integrations withDjango REST Framework, primarily through custom Serializer classes.  In most cases, you will want to use either:NaturalKeyModelSerializer, orThenatural_key_slugpseudo-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 inlookup_fieldto point directly to your natural key.NaturalKeyModelSerializerNaturalKeyModelSerializerfacilitates handling complex natural keys in your rest API.  It can be used with aNaturalKeyModel, or (more commonly) a model that has a foreign key to aNaturalKeyModelbut is not aNaturalKeyModelitself.  (One concrete example of this is thevera.Reportmodel, which has a ForeignKey tovera.Event, which is aNaturalKeyModel).NaturalKeyModelSerializerextends DRF'sModelSerializer, but usesNaturalKeySerializerfor each foreign key that points to aNaturalKeyModel.  Whenupdate()orcreate()ing the primary model, the nestedNaturalKeySerializers will automatically create instances of referenced models if they do not exist already (via thefind()method described above).  Note thatNaturalKeyModelSerializerdoes not override DRF's default behavior for other fields, whether or not they form part of the primary model's natural key.NaturalKeySerializercan technically be used as a top level serializer, though this is not recommended.NaturalKeySerializeris designed for dealing with nested natural keys and does not support updates or non-natural key fields.  Even when used together withNaturalKeyModelSerializer,NaturalKeySerializernever 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 ofNaturalKeySerializeras a specialRelatedFieldclass rather than as aSerializerper se.You can useNaturalKeyModelSerializerwithDjango REST Frameworkand/orwq.dbjust like any other serializer:# Django REST Framework usage examplefromrest_frameworkimportviewsetsfromrest_frameworkimportroutersfromnatural_keysimportNaturalKeyModelSerializerfrom.modelsimportEvent,NoteclassEventSerializer(NaturalKeyModelSerializer):classMeta:model=EventclassNoteSerializer(NaturalKeyModelSerializer):classMeta:model=NoteclassEventViewSet(viewsets.ModelViewSet):queryset=Event.objects.all()serializer_class=EventSerializerclassNoteViewSet(viewsets.ModelViewSet):queryset=Note.objects.all()serializer_class=NoteSerializerrouter=routers.DefaultRouter()router.register(r'events',EventViewSet)router.register(r'notes',NoteViewSet)# wq.db usage examplefromwq.dbimportrestfromnatural_keysimportNaturalKeyModelSerializerfrom.modelsimportEvent,Noterest.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 yourNaturalKeyModelinstances and related data.  To facilitate integration with regular HTML Forms,Django Natural Keysis integrated with theHTML JSON Formspackage, which supports nested keys via an array naming convention, as the examples below demonstrate.<formaction="/events/"method="post"><inputname="place[name]"><inputtype="date"name="date"></form>// /events.json[{"id":123,"place":{"name":"ABC123"},"date":"2016-01-01"}]<formaction="/notes/"method="post"><inputname="event[place][name]"><inputtype="date"name="event[date]"><textareaname="note"></textarea></form>// /notes.json[{"id":12345,"event":{"place":{"name":"ABC123"},"date":"2016-01-01"},"note":"This is a note"}]Natural Key SlugsAs an alternative to usingNaturalKeyModelSerializer/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.classPlace(NaturalKeyModel):name=models.CharField(max_length=255,unique=True)classRoom(NaturalKeyModel)place=models.ForeignKey(Place,models.ON_DELETE)name=models.CharField(max_length=255)classMeta: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:Create a regular serializer withid = serializers.ReadOnlyField(source='natural_key_slug')Setlookup_field = 'natural_key_slug'on yourModelViewSet(or similar generic class) and update the URL registration accordinglyEnsure foreign keys on any related models are serialized withserializers.SlugRelatedField(slug_field='natural_key_slug')Inwq.db, all three of the above can be achieved by setting the"lookup"attribute when registering with therouter:# myapp/rest.pyfromwq.dbimportrestfrom.modelsimportRoomrest.router.register_model(Room,fields='__all__',lookup='natural_key_slug',)Note that thenatural_key_slugmay not behave as expected if any of the component values contain the delimiter character (-by default).  To mitigate this, you can setnatural_key_separatoron the model class to another character.