Source code for rpaper.core.fields.hashids

from hashids import Hashids
from django.db import models
from django.utils.translation import ugettext_lazy as _


class HashidsDescriptor:
    def __init__(self, field):
        self.field = field

    def __get__(self, instance, cls=None):
        if instance is None:
            return self
        value = instance.__dict__.get(self.field.name, None)
        return None if value is None else self.field.encode_hashids(value)

    def __set__(self, instance, value):
        # Translate hashid into an integer if necessary
        if isinstance(value, str):
            value = self.field.decode_hashids(value)
        instance.__dict__[self.field.name] = value


class HashidsField(models.BigAutoField):
    """A hashids field which use autoincrement feature of DB."""

    descriptor_class = HashidsDescriptor
    description = _(
        "Hashids which automatically generated from incremental integer."
    )

    def __init__(self,
                 hashids_salt="",
                 hashids_min_length=None,
                 *args, **kwargs):
        self.hashids_salt = hashids_salt
        self.hashids_min_length = hashids_min_length
        super().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        if self.hashids_salt != "":
            kwargs['hashids_salt'] = self.hashids_salt
        if self.hashids_min_length is not None:
            kwargs['hashids_min_length'] = self.hashids_min_length
        return name, path, args, kwargs

    def contribute_to_class(self, cls, name, **kwargs):
        super().contribute_to_class(cls, name, **kwargs)
        setattr(cls, self.name, self.descriptor_class(self))

    def from_db_value(self, value, expression, connection, context):
        if value is None:
            return None
        elif not isinstance(value, int):
            raise AttributeError(
                "Non integer value %s is stored in db." % value,
            )
        return self.encode_hashids(value)

    def to_python(self, value):
        if value is None:
            return None
        elif isinstance(value, str):
            return value
        return self.encode_hashids(value)

    def get_prep_value(self, value):
        if not isinstance(value, str):
            raise AttributeError(
                "Unexpected value %s has specified." % value,
            )
        value = self.decode_hashids(value)
        return super().get_prep_value(value)

    def get_hashids(self):
        if not hasattr(self, '_hashids'):
            self._hashids = Hashids(
                salt=self.hashids_salt,
                min_length=self.hashids_min_length
            )
        return self._hashids

    def encode_hashids(self, value):
        hashids = self.get_hashids()
        return hashids.encode(value)

    def decode_hashids(self, value):
        hashids = self.get_hashids()
        numbers = hashids.decode(value)
        if len(numbers) != 1:
            raise AttributeError(
                "An invalid hashid %s has specified (%s)" % (
                    value, numbers,
                )
            )
        return numbers[0]