Need help with django-hashid-field?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

nshafer
263 Stars 27 Forks MIT License 219 Commits 2 Opened issues

Description

Django Model Field that uses Hashids to obscure the value

Services available

!
?

Need anything else?

Contributors list

# 232,207
Elixir
Python
django-...
Django
187 commits
# 461,800
Rust
Shell
django-...
Django
5 commits
# 433,353
Shell
TeX
django-...
Django
4 commits
# 6,098
Python
Django
backpor...
sentry
2 commits
# 513,549
Python
django-...
Django
HTML
2 commits
# 108,047
sony
oauth2
python-...
python-...
2 commits
# 218,651
HTML
CSS
Shell
Django
1 commit
# 329,098
CSS
HTML
uploads
s3
1 commit
# 109,363
aws-lam...
lambda
HTML
Flask
1 commit
# 657,913
django-...
Django
HTML
hashids
1 commit

.. image:: https://travis-ci.org/nshafer/django-hashid-field.svg?branch=master :target: https://travis-ci.org/nshafer/django-hashid-field .. image:: https://badge.fury.io/py/django-hashid-field.svg :target: https://badge.fury.io/py/django-hashid-field

Django Hashid Field

A custom Model Field that uses the

Hashids 
_
library 
_ to obfuscate an IntegerField or AutoField. It can be used in new models or dropped in place of an existing IntegerField, explicit AutoField, or an automatically generated AutoField.

Features

  • Stores IDs as integers in the database
  • Allows lookups and filtering by hashid string or Hashid object and (optionally) integer.
  • Can enable integer lookups globally or per-field
  • Can be used as sort key
  • Allows specifying a salt, min_length and alphabet globally
  • Supports custom salt, min_length, alphabet, prefix and allowintlookup settings per field
  • Allows prefixing hashids with custom string, e.g.
    prefix="user_"
    for hashids like "user_h6ks82g"
  • Can drop-in replace an existing IntegerField (HashidField) or AutoField (HashidAutoField)
  • Supports "Big" variants for large integers: BigHashidField, BigHashidAutoField
  • Supports Django 3.2 setting
    DEFAULT_AUTO_FIELD = 'hashid_field.BigHashidAutoField'
  • Supports Django REST Framework Serializers
  • Supports exact ID searches in Django Admin when field is specified in search_fields.
  • Supports common filtering lookups, such as
    __iexact
    ,
    __contains
    ,
    __icontains
    , though matching is the same as
    __exact
    .
  • Supports subquery lookups with
    field__in=queryset
  • Supports other lookups:
    isnull
    ,
    gt
    ,
    gte
    ,
    lt
    and
    lte
    .
  • Supports hashing operations so the fields can be used in Dictionaries and Sets.

Requirements

This module is tested and known to work with:

  • Python 3.6, 3.7, 3.8
  • Django 2.2, 3.1, 3.2
  • Hashids 1.3
  • Django REST Framework 3.12

Please Note: Python 2.x is at its end of life and is no longer supported.

Installation

Install the package (preferably in a virtualenv):

.. code-block:: bash

$ pip install django-hashid-field

Configure a global SALT for all HashidFields to use by default in your settings.py. (Note: Using a global salt for all fields will result in IDs from different fields/models being the same. If you want to have unique hashid strings for the same id, then also configure per-field salts as described in Field Parameters below.)

.. code-block:: python

HASHID_FIELD_SALT = "a long and secure salt value that is not the same as SECRET_KEY"
# Note: You can generate a secure key with:
#     from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())

Add it to your model

.. code-block:: python

from hashid_field import HashidField

class Book(models.Model): reference_id = HashidField()

Migrate your database

.. code-block:: bash

$ ./manage.py makemigrations
$ ./manage.py migrate

Basic Usage

Use your field like you would any other, for the most part. You can assign integers:

.. code-block:: python

>>> b = Book()
>>> b.reference_id = 123
>>> b.reference_id
Hashid(123): OwLxW8D

You can assign valid hashids. It's valid only if it can be decoded into an integer based on your settings:

.. code-block:: python

>>> b.reference_id = 'r8636LO'
>>> b.reference_id
Hashid(456): r8636LO

You can access your field with either hashid strings or Hashid objects:

.. code-block:: python

>>> Book.objects.filter(reference_id='OwLxW8D')
]>
>>> b = Book.objects.get(reference_id='OwLxW8D')
>>> b

>>> h = b.reference_id
>>> h
Hashid(123): OwLxW8D
>>> Book.objects.filter(reference_id=h)

You can lookup objects with integers if you set

HASHID_FIELD_ALLOW_INT_LOOKUP = True
or
allow_int_lookup=True
as a parameter to the field.

.. code-block:: python

reference_id = HashidField(allow_int_lookup=True)

Now integer lookups are allowed. Useful if migrating an existing AutoField to a HashidAutoField, but you need to allow lookups with older integers.

.. code-block:: python

>>> Book.objects.filter(reference_id=123)
]>

By default, the objects returned from a HashidField are an instance of the class Hashid (this can be disabled globally or per-field), and allow basic access to the original integer or the hashid:

.. code-block:: python

>>> from hashid_field import Hashid
>>> h = Hashid(123)
>>> h.id
123
>>> h.hashid
'Mj3'
>>> print(h)
Mj3
>>> repr(h)
'Hashid(123): Mj3'

Hashid Auto Field

Along with

HashidField
there is also a
HashidAutoField
that works in the same way, but that auto-increments just like an
AutoField
.

.. code-block:: python

from hashid_field import HashidAutoField

class Book(models.Model): serial_id = HashidAutoField(primary_key=True)

The only difference is that if you don't assign a value to it when you save, it will auto-generate a value from your database, just as an AutoField would do. Please note that

HashidAutoField
inherits from
AutoField
and there can only be one
AutoField
on a model at a time.

.. code-block:: python

>>> b = Book()
>>> b.save()
>>> b.serial_id
Hashid(1): AJEM7LK

It can be dropped into an existing model that has an auto-created AutoField (all models do by default) as long as you give it the same name and set

primary_key=True
. So if you have this model:

.. code-block:: python

class Author(models.Model):
    name = models.CharField(max_length=40)

Then Django has created a field for you called 'id' automatically. We just need to override that by specifying our own field with primary_key set to True.

.. code-block:: python

class Author(models.Model):
    id = HashidAutoField(primary_key=True)
    name = models.CharField(max_length=40)

And now you can use the 'id' or 'pk' attributes on your model instances:

.. code-block:: python

>>> a = Author.objects.create(name="John Doe")
>>> a.id
Hashid(60): N8VNa8z
>>> Author.objects.get(pk='N8VNa8z')

In Django 3.2 a new setting, "DEFAULTAUTOFIELD" was added to change all auto-generated AutoFields to a specific class. This is fully supported with django-hashid-field, and can be enabled with:

.. code-block:: python

DEFAULT_AUTO_FIELD = 'hashid_field.HashidAutoField'
DEFAULT_AUTO_FIELD = 'hashid_field.BigHashidAutoField'

Care must be given, as this will alter ALL models in your project. Usually you would only set this in a new project. Also, since this changes the auto-generated field, only global settings will be used for that field. If you desire specific settings for different models, then using this setting is not advised.

Global Settings

HASHIDFIELDSALT ~~~~~~~~~~~~~~~~~

You can optionally set a global Salt to be used by all HashFields and HashidAutoFields in your project. Do not use the same string as your SECRETKEY, as this could lead to your SECRETKEY being exposed to an attacker. Please note that changing this value will cause all HashidFields to change their values, and any previously published IDs will become invalid. Can be overridden by the field definition if you desire unique hashid strings for a given field, as described in Field Parameters below.

:Type: string :Default: "" :Note: The upstream hashids-python library only considers the first 43 characters of the salt. :Example: .. code-block:: python

    HASHID_FIELD_SALT = "a long and secure salt value that is not the same as SECRET_KEY"

HASHIDFIELDMIN_LENGTH ~~~~~~~~~~~~~~~~~~~~~~~

Default minimum length for (non-Big) HashidField and AutoHashidField. It is suggested to use 7 for HashidField and HashidAutoField, so that all possible values (up to 2147483647) are the same length.

:Type: integer :Default: 7 :Example: .. code-block:: python

    HASHID_FIELD_MIN_LENGTH = 20

HASHIDFIELDBIGMINLENGTH ~~~~~~~~~~~~~~~~~~~~~~~~~~~

Default minimum length for BigHashidField and BigHashidAutoField. It is suggested to use 13 for BigHashidField and BigHashidAutoField, so that all possible values (up to 9223372036854775807) are the same length.

:Type: integer :Default: 13 :Example: .. code-block:: python

    HASHID_FIELD_BIG_MIN_LENGTH = 30

HASHIDFIELDALPHABET ~~~~~~~~~~~~~~~~~~~~~~~

The default alphabet to use for characters in generated Hashids strings. Must be at least 16 unique characters.

:Type: string :Default: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" :Example: .. code-block:: python

    HASHID_FIELD_ALPHABET = "0123456789abcdef"

HASHIDFIELDALLOWINTLOOKUP ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Allow lookups or fetches of fields using the underlying integer that's stored in the database. Disabled by default to prevent users from being to do a sequential scan of objects by pulling objects by integers (1, 2, 3) instead of Hashid strings ("Ba9p1AG", "7V9gk9Z", "wro12zm"). Can be overridden by the field definition.

:Type: boolean :Default: False :Example: .. code-block:: python

    HASHID_FIELD_ALLOW_INT_LOOKUP = True

HASHIDFIELDLOOKUP_EXCEPTION ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

By default any invalid hashid strings or integer lookups when integer lookups are turned off will result in an EmptyResultSet being returned. Enable this to instead throw a ValueError exception (similar to the behavior prior to 2.0).

:Type: boolean :Default: False :Example: .. code-block:: python

    HASHID_FIELD_LOOKUP_EXCEPTION = True

HASHIDFIELDENABLEHASHIDOBJECT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The default behavior is to return an instance of the Hashid object (described below) in each instance of your Model. This makes it possible to get both the integer and hashid version of the field. However, other django modules, serializers, etc may be confused and not know how to handle a Hashid object, so you can turn them off here. Instead, a string of the hashid will be returned, and a new attribute with the suffix

_hashid
will be created on each instance with the Hashid object. So if you have
key = HashidField(...)
then
key_hashid
will be created on each instance. Can be overriden by the field definition.

:Type: boolean :Default: True :Example: .. code-block:: python

    HASHID_FIELD_ENABLE_HASHID_OBJECT = False

HASHIDFIELDENABLE_DESCRIPTOR ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

By default a Hashid*Field on a model will replace the original value returned from the database with a Descriptor that attempts to convert values that are set on that field of an instance with a new Hashid object (or string if ENABLEHASHIDOBJECT is False), regardless if you set an integer or a valid hashid. For the most part this is completely invisible and benign, however if you have issues due to this descriptor, you can disable it here, or on the field, and the raw value will not be replaced with the Descriptor. Can be overriden by the field definition.

:Type: boolean :Default: True :Example: .. code-block:: python

    HASHID_FIELD_ENABLE_DESCRIPTOR = False

Field Parameters

Besides the standard field options, there are settings you can tweak that are specific to HashidField and AutoHashidField.

Please note that changing any of the values for

salt
,
min_length
,
alphabet
or
prefix
will affect the obfuscation of the integers that are stored in the database, and will change what are considered "valid" hashids. If you have links or URLs that include your HashidField values, then they will stop working after changing any of these values. It's highly advised that you don't change any of these settings once you publish any references to your field.

salt ~~~~

Local overridable salt for hashids generated specifically for this field. Set this to a unique value for each field if you want the IDs for that field to be different to the same IDs on another field. e.g. so that

book.id = Hashid(5): 0Q8Kg9r
and
author.id = Hashid(5): kp0eq0V
. Suggestion:
fieldname = HashIdField(salt="modelname_fieldname_" + settings.HASHID_FIELD_SALT)
See HASHIDFIELDSALT above.

:Type: string :Default: settings.HASHIDFIELDSALT, "" :Note: The upstream hashids-python library only considers the first 43 characters of the salt. :Example: .. code-block:: python

    reference_id = HashidField(salt="Some salt value")

min_length ~~~~~~~~~~

Generate hashid strings of this minimum length, regardless of the value of the integer that is being encoded. This defaults to 7 for the field since the maximum IntegerField value can be encoded in 7 characters with the default alphabet setting of 62 characters.

:Type: int :Default: 7 :Example: .. code-block:: python

    reference_id = HashidField(min_length=15)

alphabet ~~~~~~~~

The set of characters to generate hashids from. Must be at least 16 characters.

:Type: string of characters :Default: Hashids.ALPHABET, which is "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" :Example: .. code-block:: python

    # Only use numbers and lower-case letters
    reference_id = HashidField(alphabet="0123456789abcdefghijklmnopqrstuvwxyz")

prefix ~~~~~~

An optional string prefix that will be prepended to all generated hashids. Also affects validation, so only hashids that have this prefix will be considered correct.

:Type: String :Default: "" :Example: .. code-block:: python

    # Including the type of id in the id itself:
    reference_id = HashidField(prefix="order_")

allowintlookup ~~~~~~~~~~~~~~~~

Local field override for default global on whether or not integer lookups for this field should be allowed. See HASHIDFIELDALLOWINTLOOKUP above.

:Type: boolean :Default: settings.HASHIDFIELDALLOWINTLOOKUP, False :Example: .. code-block:: python

    reference_id = HashidField(allow_int_lookup=True)

enablehashidobject ~~~~~~~~~~~~~~~~~~~~

Local field override for whether or not to return Hashid objects or plain strings. Can be safely changed without affecting any existing hashids. See HASHIDFIELDENABLEHASHIDOBJECT above.

:Type: boolean :Default: settings.HASHIDFIELDENABLEHASHIDOBJECT, True :Example: .. code-block:: python

    reference_id = HashidField(enable_hashid_object=False)

enable_descriptor ~~~~~~~~~~~~~~~~~

Local field override for whether or not to use the Descriptor on instances of the field. Can be safely changed without affecting any existing hashids. See HASHIDFIELDENABLE_DESCRIPTOR above.

:Type: boolean :Default: settings.HASHIDFIELDENABLE_DESCRIPTOR, True :Example: .. code-block:: python

    reference_id = HashidField(enable_descriptor=False)

Hashid Class

Operations with a HashidField or HashidAutoField return a

Hashid
object (unless disabled). This simple class does the heavy lifting of converting integers and hashid strings back and forth. There shouldn't be any need to instantiate these manually.

Methods ~~~~~~~

_init(value, salt="", minlength=0, alphabet=Hashids.ALPHABET, prefix="", hashids=None): ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

:value: REQUIRED Integer you wish to encode or hashid you wish to decode :salt: Salt to use. Default: "" (empty string) :min_length: Minimum length of encoded hashid string. Default: 0 :alphabet: The characters to use in the encoded hashid string. Default: Hashids.ALPHABET :prefix: String prefix prepended to hashid strings. Default: "" (empty string) :hashids: Instance of hashids.Hashids to use for encoding/decoding instead of instantiating another.

Read-Only Properties ~~~~~~~~~~~~~~~~~~~~

id ^^

:type: Int :value: The decoded integer

hashid ^^^^^^

:type: String :value: The encoded hashid string

hashids ^^^^^^^

:type: Hashids() :value: The instance of the Hashids class that is used to encode and decode

prefix ^^^^^^

:type: String :value: The prefix prepended to hashid strings

Django REST Framework Integration

If you wish to use a HashidField or HashidAutoField with a DRF ModelSerializer, there is one extra step that you must take. Automatic declaration of any HashidFields will result in an ImproperlyConfigured exception being thrown. You must explicitly declare them in your Serializer, as there is no way for the generated field to know how to work with a HashidField, specifically what 'salt', 'min_length' and 'alphabet' to use, and can lead to very difficult errors or behavior to debug, or in the worst case, corruption of your data. Here is an example:

.. code-block:: python

from rest_framework import serializers
from hashid_field.rest import HashidSerializerCharField


class BookSerializer(serializers.ModelSerializer): reference_id = HashidSerializerCharField(source_field='library.Book.reference_id')

class Meta:
    model = Book
    fields = ('id', 'reference_id')

class AuthorSerializer(serializers.ModelSerializer): id = HashidSerializerCharField(source_field='library.Author.id', read_only=True)

class Meta:
    model = Author
    fields = ('id', 'name')

The

source_field
allows the HashidSerializerCharField to copy the 'salt', 'minlength' and 'alphabet' settings from the given field at ``appname.modelname.fieldname`` so that it can be defined in just one place. Explicit settings are also possible:

.. code-block:: python

reference_id = HashidSerializerCharField(salt="a different salt", min_length=10, alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ")

If nothing is given, then the field will use the same global settings as a Hashid*Field. It is very important that the options for the serializer field matches the model field, or else strange errors or data corruption can occur.

HashidSerializerCharField will serialize the value into a Hashids string, but will deserialize either a Hashids string or integer and save it into the underlying Hashid*Field properly. There is also a HashidSerializerIntegerField that will serialize the Hashids into an un-encoded integer as well.

Primary Key Related Fields

Any models that have a ForeignKey to another model that uses a HashidField as its Primary Key will need to explicitly define how the

PrimaryKeyRelatedField 
_ should serialize and deserialize the resulting value using the
pk_field
argument. If you don't you will get an error such as "Hashid(60): N8VNa8z is not JSON serializable". We have to tell DRF how to serialize/deserialize Hashid
Fields.

For the given

Author
model defined above that has an
id = HashidAutoField(primary_key=True)
set, your BookSerializer should look like the following.

.. code-block:: python

from rest_framework import serializers
from hashid_field.rest import HashidSerializerCharField


class BookSerializer(serializers.ModelSerializer): author = serializers.PrimaryKeyRelatedField( pk_field=HashidSerializerCharField(source_field='library.Author.id'), read_only=True)

class Meta:
    model = Book
    fields = ('id', 'author')

Make sure you pass the source field to the HashidSerializer*Field so that it can copy the 'salt', 'min_length' and 'alphabet' as described above.

This example sets

read_only=True
but you can explicitly define a
queryset
or override
get_queryset(self)
to allow read-write behavior.

.. code-block:: python

author = serializers.PrimaryKeyRelatedField(
    pk_field=HashidSerializerCharField(source_field='library.Author.id'),
    queryset=Author.objects.all())

For a ManyToManyField, you must also remember to pass

many=True
to the
PrimaryKeyRelatedField
.

HashidSerializerCharField

Serialize a Hashid*Field to a Hashids string, de-serialize either a valid Hashids string or integer into a Hashid*Field.

Parameters ~~~~~~~~~~

source_field ^^^^^^^^^^^^

A 3-field dotted notation of the source field to load matching 'salt', 'minlength' and 'alphabet' settings from. Must be in the format of "appname.modelname.fieldname". Example: "library.Book.reference_id".

salt, min_length, alphabet, prefix ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

See

Field Parameters
_

HashidSerializerIntegerField

Serialize a Hashid*Field to an integer, de-serialize either a valid Hashids string or integer into a Hashid*Field. See

HashidSerializerCharField
_ for parameters.

Development

Here are some rough instructions on how to set up a dev environment to develop this module. Modify as needed. The sandbox is a django project that uses django-hashid-id, and is useful for developing features with.

  • git clone https://github.com/nshafer/django-hashid-field.git && cd django-hashid-field
  • mkvirtualenv -a . -p /usr/bin/python3 -r sandbox/requirements.txt django-hashid-field
  • python setup.py develop
  • sandbox/manage.py migrate
  • sandbox/manage.py createsuperuser
  • sandbox/manage.py loaddata authors books editors
  • sandbox/manage.py runserver
  • python runtests.py

For any pull requests, clone the repo and push to it, then create the PR.

To install the latest development version, use:

pip install git+https://github.com/nshafer/django-hashid-field.git

LICENSE

MIT License. You may use this in commercial and non-commercial projects with proper attribution. Please see the

LICENSE 
_

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.