File: field.py

package info (click to toggle)
python-django-hashids 0.7.0-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 168 kB
  • sloc: python: 394; makefile: 3
file content (154 lines) | stat: -rw-r--r-- 5,224 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
from django.conf import settings
from django.core.exceptions import FieldError
from django.db.models import Field
from django.utils.functional import cached_property
from hashids import Hashids

from .exceptions import ConfigError, RealFieldDoesNotExistError


class HashidsField(Field):
    concrete = False
    allowed_lookups = ("exact", "iexact", "in", "gt", "gte", "lt", "lte", "isnull")
    # these should never change, even when Hashids updates
    ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
    MIN_LENGTH = 0

    def __init__(
        self,
        real_field_name="id",
        *args,
        hashids_instance=None,
        salt=None,
        alphabet=None,
        min_length=None,
        **kwargs
    ):
        kwargs.pop("editable", None)
        super().__init__(*args, editable=False, **kwargs)
        self.real_field_name = real_field_name
        self.salt = salt
        self.min_length = min_length
        self.alphabet = alphabet
        self._explicit_hashids_instance = hashids_instance

        self.hashids_instance = None
        self.attached_to_model = None

    def contribute_to_class(self, cls, name):
        self.attname = name
        self.name = name

        if getattr(self, "model", None) is None and cls._meta.abstract is False:
            self.model = cls

        if self.attached_to_model is not None:  # pragma: no cover
            raise FieldError(
                "Field '%s' is already attached to another model(%s)."
                % (self.name, self.attached_to_model)
            )
        self.attached_to_model = cls

        self.column = None

        if self.verbose_name is None:
            self.verbose_name = self.name

        setattr(cls, name, self)

        cls._meta.add_field(self, private=True)

        self.hashids_instance = self.get_hashid_instance()

    def get_hashid_instance(self):
        if self._explicit_hashids_instance:
            if (
                self.salt is not None
                or self.alphabet is not None
                or self.min_length is not None
            ):
                raise ConfigError(
                    "if hashids_instance is set, salt, min_length and alphabet should not be set"
                )
            return self._explicit_hashids_instance
        salt = self.salt
        min_length = self.min_length
        alphabet = self.alphabet
        if salt is None:
            salt = getattr(settings, "DJANGO_HASHIDS_SALT")
        if min_length is None:
            min_length = (
                getattr(settings, "DJANGO_HASHIDS_MIN_LENGTH", None) or self.MIN_LENGTH
            )
        if alphabet is None:
            alphabet = (
                getattr(settings, "DJANGO_HASHIDS_ALPHABET", None) or self.ALPHABET
            )
        return Hashids(salt=salt, min_length=min_length, alphabet=alphabet)

    def get_prep_value(self, value):
        decoded_values = self.hashids_instance.decode(value)
        if not decoded_values:
            return None
        return decoded_values[0]

    def from_db_value(self, value, expression, connection, *args):
        return self.hashids_instance.encode(value)

    def get_col(self, alias, output_field=None):
        if output_field is None:
            output_field = self
        col = self.real_col.get_col(alias, output_field)
        return col

    @cached_property
    def real_col(self):
        # `maybe_field` is intended for `pk`, which does not appear in `_meta.fields`
        maybe_field = getattr(self.attached_to_model._meta, self.real_field_name, None)
        if isinstance(maybe_field, Field):
            return maybe_field
        try:
            field = next(
                col
                for col in self.attached_to_model._meta.fields
                if col.name == self.real_field_name
                or col.attname == self.real_field_name
            )
        except StopIteration:
            raise RealFieldDoesNotExistError(
                "%s(%s) can't find real field using real_field_name: %s"
                % (self.__class__.__name__, self, self.real_field_name)
            )
        return field

    def __get__(self, instance, name=None):
        if not instance:
            return self
        real_value = getattr(instance, self.real_field_name, None)
        # the instance is not saved yet?
        if real_value is None:
            return ""
        assert isinstance(real_value, int)
        return self.hashids_instance.encode(real_value)

    def __set__(self, instance, value):
        pass

    def __deepcopy__(self, memo=None):
        new_instance = super().__deepcopy__(memo)
        for attr in ("hashids_instance", "attached_to_model"):
            if hasattr(new_instance, attr):
                setattr(new_instance, attr, None)
        # remove cached values from cached_property
        for key in ("real_col",):
            if key in new_instance.__dict__:
                del new_instance.__dict__[key]  # pragma: no cover
        return new_instance

    @classmethod
    def get_lookups(cls):
        all_lookups = super().get_lookups()
        return {k: all_lookups[k] for k in cls.allowed_lookups}


HashidField = HashidsField