Giter Club home page Giter Club logo

django-hashid-field's People

Contributors

adam-tokarski avatar adamchainz avatar alexkiro avatar bjmc avatar browniebroke avatar btimby avatar camuthig avatar frossigneux avatar hhamana avatar kolanos avatar nshafer avatar olegpesok avatar pmn avatar svartalf avatar wlonk 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  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  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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-hashid-field's Issues

Django: Instantiated objects in TestCase.setUpTestData() that use HashidAutoField get a different pk in the test methods

Asuming Foo has a HashidAutoField if used in a TestCase, and following the standard django test pattern:

from django.db import models
from hashid_field import HashidAutoField

class Foo(models.Model):
    id = HashidAutoField(
        primary_key=True, alphabet="0123456789abcdefghijklmnopqrstuvwxyz")
from django.test import TestCase

class MyTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Set up data for the whole TestCase
        cls.foo = Foo.objects.create(bar="Test")
        print (cls.foo.id) # prints a hash like 'jmvnjz7' which is pk

    def test1(self):
        # Some test using self.foo
        print (self.foo.id) # prints a DIFFERENT hash like '4rqramj'

the versions I'm using:

django-hashid-field==3.2.1
Django==3.2.5

Clarify readme about searching in the admin

The readme says: "Supports common filtering lookups, such as field__icontains so that Django Admin search_fields works out of the box."

In reality only exact matches work in the admin: the code in lookups.py tries to interpret the full string that was entered in the search box as a hashid, if it cannot then the search returns no items (or raises an exception depending on HASHID_FIELD_LOOKUP_EXCEPTION ).

Given that only an integer id is stored in the database (and not the hashid string), it makes sense that partial searches dont work (that would require generating all hashid values starting at one, and comparing to the search string). What I suggest is clarifying the docs.

Migrations and existing data

I'm wondering what the best way to handle existing db rows, and how to get the new HashidField() initialized during a migration? Setting the default value to None fails.

I saw migrations.RunPython() in the Django docs, and wondered if I could somehow pull the data and modify it in flight, but seems the column will still need a default value. Happy to update the docs if there is a standard way here.

TypeError when primary key is an OneToOneField

With the models and serializers below I get an exception when listing the books:

TypeError: '1' value must be a valid Hashids string.

Removing the nested serialization of authors in the book serialization gets rid of the exception but displays an invalid id for the author of each book.

Models

# books/models.py
from django.db import models
from hashid_field import HashidAutoField

class Book(models.Model):
    id = HashidAutoField(primary_key=True)
    name = models.CharField(max_length=255)
    author = models.ForeignKey('authors.Author', on_delete=models.SET_NULL, related_name='books', null=True)
# customauth/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
from hashid_field import HashidAutoField

class User(AbstractUser):
    id = HashidAutoField(primary_key=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
# authors/models.py

from django.conf import settings
from django.db import models

class Author(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, primary_key=True)
    description = models.TextField(max_length=1000)

Serializers

# authors/serializers.py
from django.conf import settings
from django.contrib.auth import get_user_model
from hashid_field.rest import HashidSerializerCharField
from rest_framework import serializers
from .models import Author

class UserSerializer(serializers.HyperlinkedModelSerializer):
    id = HashidSerializerCharField(source_field='%s.id' % settings.AUTH_USER_MODEL)

    class Meta:
        model = get_user_model()
        fields = ('url', 'id', 'username')

class AuthorSerializer(serializers.HyperlinkedModelSerializer):
    user = UserSerializer()

    class Meta:
        model = Author
        fields = ('url', 'user', 'description',)
# books/serializers.py
from hashid_field.rest import HashidSerializerCharField
from rest_framework import serializers
from authors.serializers import AuthorSerializer
from .models import Book

class BookSerializer(serializers.HyperlinkedModelSerializer):
    id = HashidSerializerCharField(source_field='books.Book.id')
    author = AuthorSerializer()

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

Django 3.0 support

Upon updating to Django 3.0, the following import error is raised on server startup:

File "C:\dev\myproject\venv\lib\site-packages\hashid_field\hashid.py", line 4, in <module>
    from django.utils import six
ImportError: cannot import name 'six' from 'django.utils' (C:\dev\myproject\venv\lib\site-packages\django\utils\__init__.py)

It appears django.utils indeed does not include a six module anymore. Possibly since I am updating an existing project, it was already included in my dependencies as such: six==1.12.0.
It was then possible to directly change the import in both hashid_field.hashid and hashid_field.rest.

from django.util import six

to simply

import six

This fixed it, and I haven't noticed any other error related to django-hashid-field.

Declaring six as an independent dependency instead of relying on Django' reexport may fix it for everyone with no backwards compatibility issue for users not upgrading their Django yet.

At least that's the easy fix. Considering six itself is a Python 2/3 compatibility library, and Django's removal is due to dropping support for Python 2.7, as it reaches end of LTS in January 2020, it might be possible to consider dropping it here too, as suggested in #35.

Error when deleting object - can't adapt type 'Hashid'

Hi, I got this error when trying to delete an object with HashidAutoField as primary key. Here is my code:

my_object = form.save(commit=False)  # i am using model form
my_object.field1 = 'field1_value'
my_object.field2 = 'field2_value'
my_object.save()
my_object.delete()  # this will cause error

and here is how I use hashid on my model definition:

id = HashidAutoField(primary_key=True, salt=HASHID_SALT, alphabet=HASHID_ALPHABET)

I wonder what caused the error

Using hashid-field on third party models

Hi!

I've have used some fields as HashidAutoField on my own models, and works very well.
Because of this I want to hash the id's of other models of third party apps. There's any functionality to accomplish this, or pattern that allows to use hashid-fields? I've searched every corner of Google, but options as subclassing of models or proxy are not valid, because id field exists on main Model, and can't be overridden.
On the other hand, downloading the source code of an application just to edit his model and include this feature does not seem appropriate. I'm sure that an easy solution can be applied.

Thanks!

Automatic Per-Model/Field Salt

While you can specify a per-field salt, it'd be slick to include the model & field information into a default salt. Currently:

class Model1(models.Model):
    id = HashIdField()

class Model2(models.Model)
    id =  HashIdField()

m1 = Model1.objects.create(id=7)
m2 = Model2.objects.create(id=7)

m1.id => "hjGh7"
m2.id => "hjGh7"

Which reveals unnecessarily that m2.id == m1.id as integers.

My idea is:

class Model1(models.Model):
    id = HashIdField(salt=HashIdField.AUTO)

class Model2(models.Model)
    id = HashIdField(salt=HashIdField.AUTO)

m1 = Model1.objects.create(id=7)
m2 = Model2.objects.create(id=7)

m1.id => "hj9dGh7"
m2.id => "a7rr2eY"

An implementation could look a bit like:

import hashlib

class HashidFieldMixin(object):
    AUTO = object()
 
    @classmethod
    def build_auto_salt(cls, *args):
        return hashlib.sha1(
            "|".join(args)).encode()
        ).hexdigest()

    def contribute_to_class(self, *args, **kwargs):
        super(HashidFieldMixin, self).contribute_to_class(*args, **kwargs)
        if self.salt is self.AUTO:
            # set the field salt automatically based on the global salt, the model, and the field name
            self.salt = self.build_auto_salt(
                settings.HASHID_FIELD_SALT,
                self.model._meta.label_lower,  # Model label. eg: "myapp.mymodel"
                self.name  # Field name
            )

Important caveat is that if you rename your model or field (or change your global salt), you'd need to set salt explicitly to match the old value. But with the helper, that'd be relatively painless:

class RenamedModel1(models.Model)
    renamed_id = HashIdField(salt=HashIdField.build_auto_salt(settings.HASHID_FIELD_SALT, "myapp.model1", "id"))

Another way to implement this is via #21 by also including prefix into the salt in addition to using it as a prefix.

Shouldn't invalid hashid throw a DoesNotExist instead of a TypeError

I am using a hash-id field for one of my models in a small private project. The Hash-ID is used in URLs to access the model instance. I recently had to change the HASHID_FIELD_SALT essentially invalidating all existing links, but as it is only a small project, I didn't worry about it too much.

Now some of my users still had old links but as I use get_object_or_404 to retrieve the model from the database, I thought I'd be save, as I thought the users with the old links would simply get a 404 error. This is, however, not the case. The Hash-ID Field throws a TypeError: value must be a positive integer or a valid Hashids string on a old Hash-ID. As the Hash-ID has the same length as the expected Hash-ID, I would expect the Model to throw a DoesNotExist Exception that would be caught by get_object_or_404.

Is this behavior desired? Otherwise I would propose to change the behavior to throw a DoesNotExist if the hash-id is of the expected length but not valid.

I can also provide a pull-request for this issue if you'd consent to my proposal.

Substitute a CharField with a HashidAutoField

I am currently using the hashidslibrary to generate a friendly ID from the object primary key. Something similar to the following example taken from djangosnippets

 def save(self, *args, **kwargs):
    super(MyModel, self).save(*args, **kwargs)

    # Populate the invoice_id if it is missing
    if self.id and not self.invoice_id:
        self.invoice_id = friendly_id.encode(self.id)
        super(MyModel, self).save(*args, **kwargs)

That is quite an ugly solution. Plus I made the same mistake described in #1. I would like to adopt this package but I have two requirements:

  1. Keep the old hashids stored in a CharField
  2. Instances of different models must have different hashids

Is that possible? To avoid collision with old hashids I could set a different length for the new ones but I don't know how to properly integrate it.

HashidAutoField auto-increments just like an AutoField

https://github.com/nshafer/django-hashid-field#hashid-auto-field
It's a little confusing u said that "Along with HashidField there is also a HashidAutoField that works in the same way, but that auto-increments just like an AutoField"
So due it means that successive hash values will have unit difference which will be the same in all cases.
I am expecting it won't be like that but it read up the same.
And if so will in not pose a security concern.
M a little confused.

`icontains` filtering is not working

Model.objects.filter(id__icontains='Vn6N7n4') is working fine. But if I lower() string this is not working. Same with Model.objects.filter(id__icontains='V')

subquery has too many columns

Had issue described on DjangoProject issue tracker, but just after switching to use this battery (django-hashid-field). So decided to share and save time for others.
https://code.djangoproject.com/ticket/30047?cversion=0&cnum_hist=9

A Subquery filter on models.Obj, e.g., filter(obj__in=Subquery(...)) ,requires that the subquery return exactly one column populated with Obj.pk.

Documentation suggests using objects.values('pk')

However, if the models.Obj.Meta.ordering is set, then those ordering fields are included with the subquery results, raising the exception:
django.db.utils.ProgrammingError: subquery has too many columns
This is expected behavior, but confusing and not directly obvious in the code. Could be more clearly documented at:
https://docs.djangoproject.com/en/dev/ref/models/expressions/#limiting-a-subquery-to-a-single-column

Can be closed.

Salt only uses first 43 characters

This appears to be an issue upstream (which I raised there: davidaurelio/hashids-python#43), but may be worth knowing (and maybe documenting) here too. Advice such as:

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

...isn't strictly true. Only the first 43 characters of whatever salt you supply are actually used. It remains to be seen if this is a bug, or undocumented feature (or me misunderstanding), but may save someone some time if they're trying things like salt=settings.HASHID_FIELD_SALT + 'something_else' in an attempt to have multiple salts in their application.

Security issue on lookup using strings

>>> get_user_model().objects.get(id=1)
*** api.models.user.User.DoesNotExist: User matching query does not exist.
>>> get_user_model().objects.get(id='1')  # security issue
<User: user0>
>>> get_user_model().objects.get(id='BKZv4G8')
<User: user0>

get_user_model().objects.get(id='1') should raise an exception given that it can lead to a security breach (for example an attacker can find how many rows there is in the DB). And if str(integer) lookup is allowed, the option HASHID_FIELD_ALLOW_INT_LOOKUP is useless...
The release 3.2.0 has this security issue while the 3.1.3 works perfectly.

I fixed that in the PR.

Using get_object_or_404 with django-hashid-field

Hi all,
I am using the well known shortcut https://docs.djangoproject.com/en/3.2/topics/http/shortcuts/#get-object-or-404 to check if the object exist with the given hashid.
This is a part of my model:

class QRCode(models.Model):
    serial_id = BigHashidAutoField(primary_key=True, min_length=24)

For example:
<QRCode: Gv961aN2Lry8mWR4EX8YA5MJ>

Now the problem is that if I query the DB with the serial_id in lower case, "gv961an2lry8mwr4ex8ya5mj", the get_object_or_404() returns me the object.

qr = get_object_or_404(QRCode, pk=serial_id)

Is it something related to django_hashid_field or to standard Django ?
Thanks for the support.

The values_list and tight coupling to the database

I've been evaluating this project as a way to obfuscate my internal identifiers. I think the work here is really good, so thank you so much for this tool and your work on this. As I've been evaluating this in my own project, I noticed that low level functionality like values_list doesn't actually provide the values but instead provides the Hashid object. For example:

In [1]: books = Book.objects.all()[:10]                                                                                                                                                                 

In [2]: books.values_list('id', flat=True)                                                                                                                                                              
Out[2]: <QuerySet [Hashid(3): MzRKGR7, Hashid(6): WYjEbNm, Hashid(9): LVjZvj2, Hashid(12): P1a9rRX, Hashid(14): 0GRXVNL, Hashid(17): 1rNOMRY, Hashid(19): m6a6kRD, Hashid(21): JLN7QRv, Hashid(23): JoNYnaV, Hashid(25): KVNWLaP]>

Is there any way that you know of, to have this just flatly produce either the hash IDs or the IDs? Of course this could be built into a custom manager I'm sure, but it seems like this does modify the way the database ORM typically works in Django.

This makes me think that the tight binding to the database perhaps is too tight an abstraction. If what I'm trying to do is obfuscate database identifiers so that people can't just march up my IDs or see how active my platform is, is there a way to just use Hashids as some sort of middleware to just translate between id and hashid without being tightly coupled to the database?

DRF Serializer Extensions takes this approach, but I thought that the execution of the idea was far too confusing and required a lot of additional buy in towards using their other structures, which I wasn't interested in.

I really love the simplicity of what you have created here and the ease in which it can be implemented without messing with my database IDs. I was just wondering what other limitations beyond values_list may I encounter having this tied so closely to the database as a field.

Have you considered this as an extension where you can just implement this at the serializer level without touching models or migrations (with full understand that it's an abstraction)? Thanks again.

Filtering by hash id lookup

can anyone explain this to me:

"Supports common filtering lookups, such as __iexact, __contains, __icontains, though matching is the same as __exact."

hash = 'La8yq8O'
filter(hash__icontains='La' -> False
filter(hash__icontains='La8yq8O' -> True

is this the part "though matching is the same as __exact" if so then it does not support the other filter
is there a way around this?

Fixtures error

I'm getting an error while trying to load fixtures after initial migration (migration for existing data works fine)

Error as result of python manage.py loaddata initial_data:

TypeError: Problem installing fixture '/project/app/fixtures/initial_data.json': Hashid(1): HASH is not JSON serializable

model.py:

class Object(models.Model):
    id = HashidAutoField(
        primary_key=True,
        min_length=6
    )

fixture:

[
  {
    "model": "app.object",
    "pk": 1,
    "fields": {
     …
    }
  }
]

Reduced alphabet size when initializing a Hashid object and the `hashids` argument is not None

This commit a8e8366 seems to be incorrectly copying the alphabet at line 19 (self._alphabet = hashids._alphabet).

In Hashids the alphabet does not include separators so the alphabet argument passed into the constructor of Hashids is different from self._alphabet.

A common input alphabet is 0123456789abcdef. After separators are removed, hashids._alphabet will have fewer than the minimum of 16 characters, which is possibly the cause for #44

The old code didn't have this problem because it was not reading hashids._alphabet

IntegrityError for ForeignKey in DRF Request POST

Hello,
I've just installed this package in my project and after following the guide, I'm getting an Integrity Error when performing POST requests.
I'll use an example to demo:

#models.py
class Book(models.Model):
	id = HashidAutoField(primary_key=True)
	title = models.CharField(max_length=200)
	isbn = models.CharField(max_length=50)
	author = models.ForeignKey('Author', related_name='book')

class Author(models.Model):
	id = HashidAutoField(primary_key=True)
	name = models.CharField(max_length=100)
	books = models.ForeignKey(Book, related_name="author")
#serializers.py
class BookSerializer(serializers.ModelSerializer):
	id = HashidSerializerCharField(source_field='my_app.Book.id')
	author = serializers.PrimaryKeyRelatedField(pk_field=HashidSerializerCharField(source_field='my_app.Author.id'), read_only=True)
    
    class Meta:
        model = Book
        fields = ("id", "title", "isbn", "author")


class AuthorSerializer(serializers.ModelSerializer):
    id = HashidSerializerCharField(source_field='my_app.Author.id')
    
    class Meta:
        model = Author
        fields = ("id", "name")
#views.py
class BooksList(APIView):
	def get(self, request, format=None):
		a_list = Book.objects.all()
		serializer = BookSerializer(a_list, many=True)
		return Response(serializer.data)

	def post(self, request, format=None):
		serializer = BookSerializer(data=request.data)
		if serializer.is_valid():
			serializer.save()
			return Response(serializer.data, status=status.HTTP_201_CREATED)
		return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

	def delete(self, request, pk, format=None):
		item = self.get_object(pk)
		item.delete()
		return Response(status=status.HTTP_204_NO_CONTENT)

class BookDetail(APIView):
	def get_object(self, pk):
        try:
            return Book.objects.get(pk=pk)
        except Book.DoesNotExist:
            raise Http404

    def get(self, request, pk, format=None):
        item_list = self.get_object(pk)
        item_list = BookSerializer(item_list)
        return Response(item_list.data)

    def put(self, request, pk, format=None):
        item = self.get_object(pk)
        serializer = Book(item, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk, format=None):
        item = self.get_object(pk)
        item.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
#urls.py
url_patterns = [
....
    url(r'^api/books/$', views.BooksList.as_view()),
    url(r'^api/books/(?P<pk>\w+)/$', views.BookDetail.as_view()),
....

I use token authentication for my APIs,
Now when I do a POST
curl -X POST 'http:localhost:8000/my_app/books/' -H "Authorizations: Token my_token_here" -d "title=TestBook&isbn=1234567890&author=7a5kPOq"

I get an error
IntegrityError at /my_app/api/books/ NOT NULL constraint failed:my_app_book.author_id Request Method: POST Request URL: http://localhost:8000/my_app/api/books/
(I have an author whose id = 7a5kPOq)

django == 1.11.6
django-hashid-fields == 2.0.1

The same worked before I introduced the HashidAutoField. Why can't it recognize the

Not clear on how to serialize a ForeignKey to a model using HashidAutoField

I was getting serialization errors as the serialize didn't know how to serialize the foreign models PK.

As a temporary workaround, I did this, but I suspect it's not the best way:

class HashidSerializerRelatedField(serializers.RelatedField):

	def to_representation(self, value):
		return str(value.id)
	
	pass

Serializer accepts int parameter although allow int lookup is false

Hello,

I have observed that the serializer converts a int parameter to object, even if int lookup is forbidden.
My serializer is:
foo = serializers.PrimaryKeyRelatedField(pk_field=HashidSerializerCharField(), queryset=Foo.objects.all(), required=False)

Then I have a validate method:
def validate_foo(self, value)

Both integer and string parameter triggers the validate_foo method with the retrieved object in argument.
However, HASHID_FIELD_ALLOW_INT_LOOKUP is False.

This leads to a security hole, where a user can retrieve the integer behind the obfuscated id of its objects.

Add Django path converter for HashID

Created Django path converter using official documentation.
I use it instead of slug, because last one allows extra symbols which breaks desired logic.

Probably it can be useful for somebody else. If needed I can create a PR for this project.

""" Django file `blog_app/urls.py` """

from django.urls import path, register_converter

from . import views
from .constants import HASHID_ALPHABET, HASHID_MIN_LENGTH

# Example:
# HASHID_ALPHABET = 'abcdefABCDEF1234567890'
# HASHID_MIN_LENGTH = 7


class HashidPathConverter:
    regex = '[' + HASHID_ALPHABET + ']{' + str(HASHID_MIN_LENGTH) + ',}'

    @staticmethod
    def to_python(value):
        return value

    @staticmethod
    def to_url(value):
        return str(value)


register_converter(HashidPathConverter, 'hashid')


urlpatterns = [
    # ...
    path('blog/<hashid:post_id>--<slug:slug>/', views.view_blog_post, name='blog_post'),
    # ...
]

Remove Python 2.7 support

Official support for Python 2.7 ends in two months, after which no bug fixes or security updates will be released. In preparation for that, I think it is time to remove code from Django Hashid field related to Python 2.7, as well as removing Python 2.7 from the CI test matrix.

The following checklist can tracks the progress of this endeavour:

  • remove Python 2 support in .travis.yml and tox.ini
  • remove Python 2 classifiers in setup.py
  • remove six from the code
  • remove __future__ statements from the code
  • update README.rst accordingly
  • convert super() calls to python 3 style

HashID value changes every time I restart dev server

This is highly annoying & counterproductive. I'm trying to test social share functionality using hash IDs in the URL link. But the links become useless if I need to restart my dev server. Why does it change HashID values ALREADY STORED INSIDE THE DATABASE?

What's the best way of handling these in forms.

I have various places where id's come in through forms, do you have any guidance on how to validate these at the form level so as to avoid TypeErrors when looking them up in the ORM

TypeError: 'bob' value must be a positive integer or a valid Hashids string.

Error using collector method

Hi,
when I run this code:

from django.db.models.deletion import Collector
collector = Collector(using='default')
collector.collect([obj])

(obj is based on a model with Hashid as primary key)

I get:

django.db.utils.InterfaceError: Error binding parameter 0 - probably unsupported type.

HashidAutoField error when using get_or_create shortcut

When I use Django get_or_create shortcut with an HashidAutoField, I get the error below:

AttributeError: 'NoneType' object has no attribute '__dict__'

But when I use Django AutoField, it works perfectly.

Example of use to generate the error:

# models.py
class Channel(models.Model):
    id = HashidAutoField(primary_key=True)
    name = models.CharField(max_length=60)

and then execute:

Channel.objects.get_or_create(name='test')

Comparing to strings

There are a bunch of situations where it would be convenient if str(hashid) == hashid was True.
I was wondering if there was a particular reason why this is not supported currently?

Searching for hashid's with the ModelAdmin's 'search_fields' feature?

It would be very useful for our daily operations if we could search the hashed value in the standard ModelAdmin search widget.

I naively tried adding the field name to search_fields but it doesn't find the items I know are there. This was with an HashidAutoField with primary, salt and alphabet all set.

I've spend some downtime looking into it, and found ModelAdmin.get_search_results in the docs, and I can make this search work by implementing it like this:

def get_search_results(self, request, queryset, search_term):
    queryset, use_distinct = super().get_search_results(request, queryset, search_term)
    queryset |= self.model.objects.filter(id=search_term)
    return queryset, use_distinct

What is weird about it that my code doesn't do any decoding; the regular queryset lookup on the field does it already.

So now I wonder if we can make it so we don't need to cart this overridden method around but just use the field name in the search_fields?

Lookups fail for autofield when the base id is zero

In hashid_field/hashid.py I think it should read like:

        value = self.decode(id)
        # NB value could be zero
        if value is not None:
            self._id = value
            self._hashid = id

else if value=0 like it would be for an AutoField starting from zero, then decode appears to fail and it leads to valueerror even though the decode actually returned a non-null value

Error in HashidAutoField example in readme?

If I follow this example from the readme:

from hashid_field import HashidAutoField

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

I get an error "A model can't have more than one AutoField." (likely because django created an 'id' autofield itself). This is on Django 1.11 and django-hashid-field 1.2.1

It need primary_key=True at least.

All numeric encoded hashids being treated as natural IDs

Hi, I have run into an issue whereby an object has a fully-numeric coded hashid. Despite a comment in the library asserting this is not possible I have found that it is. Here are the steps to recreate:

import hashids
a = hashids.Hashids("salt", min_length=7, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
a.encode(8308830)
>>> '5666668'

I realise now that this may have been avoided if we used a prefix, but unfortunately we did not.

I think the issue is that the _is_uint() method used by the Hashid class assumes if a value can be cast to an unsigned integer, that it needs to be encoded rather than decoded. This means when we are deserialising a numeric hashid to try and resolve a foreign key relation (we are using DRF) the numeric hashid is being treated as a natural ID, and the wrong object is queried.

I'm not sure what the solution for this is. Would it be possible to add a setting e.g. HASHID_FIELD_RESPECT_TYPE or something (defaults to false) and then write the _is_unit() method like this:

from .conf import settings

def _is_uint(number):
    """Returns whether a value is an unsigned integer."""
    if settings.HASHID_FIELD_RESPECT_TYPE:
        return isinstance(number, int) and int(number) >= 0
    try:
        return number == int(number) and number >= 0
    except ValueError:
        return False

HashidAutoField and REST post requests

Hi,
in Django Rest Framework, I normally don't have to declare id fields explicitly. When I post data, a new instance is created. Sadly, this does not work if I make my model id a HashidAutoField:

id = HashidAutoField(primary_key=True) 

In the ModelSerializer, I have to declare id explicitly as well:

id = HashidSerializerCharField(source_field='api.project.id')

When I now send a post, the server responds with an ApiError:

id: This field is required.

Naturally I would not send an id, for it is a post message, intended to create a new object.
And I need the id to automatically created by the database, as usual. How is this supposed to work?

Fix RemovedInDjango30Warning from Django 2.0+

The latest pytest surfaces all deprecation warnings at the end of the test run, so I'm seeing this one hundreds of times on my app.

.../python3.6/site-packages/django/db/models/sql/compiler.py:997: RemovedInDjango30Warning: Remove the context parameter from HashidAutoField.from_db_value(). Support for it will be removed in Django 3.0.
  RemovedInDjango30Warning,

Should be an easy fix with conditional versions of the function for django versions.

Using hashids with GenericRelations and allow_int_lookup=False

I have been incorporating hashids as primary keys into my project with allow_int_lookup set to False. Everything was going really smoothly until my tests failed out on the GenericRelations. I use GenericRelations in a few areas in my project. The one area that is quite expansive is for notifications. Having notifications be GenericRelations, I can have a list of notifications that link out to different targets on my site.

Here are the three fields that are required for a generic relation (taken from my Notification model).

    target_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    target_object_id = models.PositiveIntegerField()
    target_object = GenericForeignKey('target_content_type', 'target_object_id')

After moving over to hashids, generic relationships will no longer work unless I have allow_int_lookup=True on every model that could be used in a generic relationship. This obviously makes total sense since the ContentTypes framework is going to attempt to retrieve the object given content_type and object_id.

My first thought to making this work is to make object_id a CharField instead of a PositiveIntegerField and to always have it store the hashid. My fear with this approach is that I will need to do a data migration on existing objects in the system to convert the object_id over to the corresponding hashid for that object type (every model has its own salt, so I would have to be very careful here).

It seems like the easiest solution here is to just keep allow_int_lookup=True, but this will cover a large number or models that could end up in a notification. I kind of wanted to keep this set to False just so that people can't walk ids.

Is there a recommended approach here? Thanks!

Salt based on foreign_key

Hi everyone,

I would like to implement hashid's in my project but wonder if it's possible to use a foreign_key in a model as salt for the hashid? If this is not possible, is there any other way of using a different salt for each model?

Regards,
Egbert

Using invalid hashid on related field throws ValueError even with HASHID_FIELD_LOOKUP_EXCEPTION=False

This is related to the closed issue #17 that deals only with direct lookups.

I have the default HASHID_FIELD_LOOKUP_EXCEPTION = False, which works for as expected if I look up an object by primary key directly.

However, if I try to filter a queryset by hashid to a related field (e.g. a foreign key) the queryset throws a ValueError if the hashid is not valid. I understand why, but for my purposes it shouldn't really matter whether I pass in a valid but non-existent key, or an invalid (and equally non-existent) key.

Example:

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

class Book(models.Model):
    reference_id = HashidField()
    author = models.ForeignKey('Author')

# now, if I try to do the following lookups:

# valid ID: returns the books for given author
Book.objects.filter(author_id='OwLxW8D')
# valid ID but no author with given ID, returns empty queryset
Book.objects.filter(author_id='r8636LO')
# invalid ID throws ValueError
Book.objects.filter(author_id='OwLxW8X')

Because it's quite tricky to catch invalid hashids before they are passed into the queryset (a simple regex is clearly not enough and in my case the salt is different for each model) this leads to having to handle two cases: empty results and ValueErrors. Especially inside Django Rest Frameworks internals (like filters) this is quite cumbersome, and actually I don't care whether the hashid is invalid or just doesn't exist, as long as it matches basic sanity checks with a regex.

So, my solution (a bit hacky so I'm sure there is a cleaner solution, but this seems to work at least on Postgres):

class AutoSaltHashidFieldMixin(HashidFieldMixin):
    # see my comment in #31 for automatic salt based on model, that's in this same mixin
    def get_prep_value(self, value):
        try:
            return super().get_prep_value(value)
        except ValueError:
            # don't expose errors, simply let it go through to empty results (since -1 never exists)
            return -1

# use these in your models    
class HashidAutoField(AutoSaltHashidFieldMixin, models.AutoField):
    description = "A Hashids obscured AutoField"

class HashidBigAutoField(AutoSaltHashidFieldMixin, models.BigAutoField):
    description = "A Hashids obscured BigAutoField"

Also this makes sure that invalid hashids don't trigger a different error than non-existent hashids, which might expose some information on how the hashids are built up.

Alphabet must contain at least 16 unique characters

In fresh version 3.1.2 something was changed and crashes usage of Django url reversion ({% url 'blog_post' post_id=post.id %}). Version 3.1.1 works fine.
I have custom alphabet for HashidAutoField:
id = HashidAutoField(primary_key=True, alphabet=HASHID_ALPHABET, min_length=HASHID_MIN_LENGTH)

Add ability to have a per-model prefix

It would be nice to be able to specify a per-model prefix, eg:

class User(models.Model)
    id = AutoHashId(primary_key=True, prefix='us_')
    # ids like us_3n3b3hjbjh4

class Project(models.Mode)
    id =  AutoHashId(primary_key=True, prefix='pr_')
    # ids like pr_j5f7l3hg5k5l

Usage with generic views

Hi,

Thanks for the package! When I use id = HashidAutoField(primary_key=True) on a model, it works fine, except that if I use a Django generic DetailView with this model, accessible from a url url(r'^models/(?P<pk>[0-9a-zA-Z]+)/$', views.MyView.as_view()) it works with both hashes and real IDs. Which is a problem if I understand well the purpose of hashids - prevent sequential scraping.

Am I missing something ?

Cheers

Need to support gt operator to iterate by chunks

I am using django-chunkator to chunk big querysets and save RAM.
It find the next chunk by doing filter(pk__gt=pk) (see here).
However gt operator is not supported by django-hashid-field.
Is there a particular reason to do not support it with integers?
We can get similar result by doing Model.objets.all()[start:] but I am not sure of the memory efficiency.
I opened an issue here: peopledoc/django-chunkator#36

Object of type 'Hashid' is not JSON serializable

The regular JSON encoders can't handle the Hashid object, so we had to override the encoder to use the hashed value instead of the instance:

class HashidJSONEncoder(DjangoJSONEncoder):
    def default(self, o):
        if isinstance(o, Hashid):
            return str(o)
        return super().default(o)

This is not that surprising or new, but I'd leave a note.

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.