mirror of
https://github.com/Hopiu/django-model-utils.git
synced 2026-03-16 20:00:23 +00:00
Added urlsafe token field
This commit is contained in:
parent
57d26ee0cc
commit
a56d07cd68
5 changed files with 141 additions and 0 deletions
|
|
@ -99,3 +99,4 @@
|
||||||
| zyegfryed <zyegfryed@gmail.com>
|
| zyegfryed <zyegfryed@gmail.com>
|
||||||
| Éric Araujo <merwok@netwok.org>
|
| Éric Araujo <merwok@netwok.org>
|
||||||
| Őry Máté <ory.mate@cloud.bme.hu>
|
| Őry Máté <ory.mate@cloud.bme.hu>
|
||||||
|
| Nafees Anwar <h.nafees.anwar@gmail.com>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ Unreleased
|
||||||
- Add support for `Django 3.2`
|
- Add support for `Django 3.2`
|
||||||
- Drop support for `Django 3.0`
|
- Drop support for `Django 3.0`
|
||||||
|
|
||||||
|
- Added urlsafe token field.
|
||||||
|
|
||||||
4.1.1 (2020-12-01)
|
4.1.1 (2020-12-01)
|
||||||
------------------
|
------------------
|
||||||
- Applied `isort` to codebase (Refs GH-#402)
|
- Applied `isort` to codebase (Refs GH-#402)
|
||||||
|
|
|
||||||
|
|
@ -180,3 +180,46 @@ or any other ModelForm, default is False.
|
||||||
class MyAppModel(models.Model):
|
class MyAppModel(models.Model):
|
||||||
uuid = UUIDField(primary_key=True, version=4, editable=False)
|
uuid = UUIDField(primary_key=True, version=4, editable=False)
|
||||||
|
|
||||||
|
|
||||||
|
UrlsafeTokenField
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
A ``CharField`` subclass that provides random token generating using
|
||||||
|
python's ``secrets.token_urlsafe`` as default value.
|
||||||
|
|
||||||
|
If ``editable`` is set to false the field will not be displayed in the admin
|
||||||
|
or any other ModelForm, default is False.
|
||||||
|
|
||||||
|
``max_length`` specifies the maximum length of the token. The default value is 128.
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from model_utils.fields import UrlsafeTokenField
|
||||||
|
|
||||||
|
|
||||||
|
class MyAppModel(models.Model):
|
||||||
|
uuid = UrlsafeTokenField(editable=False, max_length=128)
|
||||||
|
|
||||||
|
|
||||||
|
You can provide your custom token generator using the ``factory`` argument.
|
||||||
|
``factory`` should be callable. It will raise ``TypeError`` if it is not callable.
|
||||||
|
``factory`` is called with ``max_length`` argument to generate the token, and should
|
||||||
|
return a string of specified maximum length.
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from model_utils.fields import UrlsafeTokenField
|
||||||
|
|
||||||
|
|
||||||
|
def _token_factory(max_length):
|
||||||
|
return uuid.uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
|
class MyAppModel(models.Model):
|
||||||
|
uuid = UrlsafeTokenField(max_length=32, factory=_token_factory)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import secrets
|
||||||
import uuid
|
import uuid
|
||||||
|
from collections import Callable
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
@ -309,3 +311,41 @@ class UUIDField(models.UUIDField):
|
||||||
kwargs.setdefault('editable', editable)
|
kwargs.setdefault('editable', editable)
|
||||||
kwargs.setdefault('default', default)
|
kwargs.setdefault('default', default)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class UrlsafeTokenField(models.CharField):
|
||||||
|
"""
|
||||||
|
A field for storing a unique token in database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, editable=False, max_length=128, factory=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
editable: bool
|
||||||
|
If true token is editable.
|
||||||
|
max_length: int
|
||||||
|
Maximum length of the token.
|
||||||
|
factory: callable
|
||||||
|
If provided, called with max_length of the field instance to generate token.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
TypeError
|
||||||
|
non-callable value for factory is not supported.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if factory is not None and not isinstance(factory, Callable):
|
||||||
|
raise TypeError("'factory' should either be a callable not 'None'")
|
||||||
|
self._factory = factory
|
||||||
|
|
||||||
|
kwargs.pop('default', None) # passing default value has not effect.
|
||||||
|
|
||||||
|
super().__init__(editable=editable, max_length=max_length, **kwargs)
|
||||||
|
|
||||||
|
def get_default(self):
|
||||||
|
if self._factory is not None:
|
||||||
|
return self._factory(self.max_length)
|
||||||
|
# generate a token of length x1.33 approx. trim up to max length
|
||||||
|
token = secrets.token_urlsafe(self.max_length)[:self.max_length]
|
||||||
|
return token
|
||||||
|
|
|
||||||
55
tests/test_fields/test_urlsafe_token_field.py
Normal file
55
tests/test_fields/test_urlsafe_token_field.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from django.db.models import NOT_PROVIDED
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from model_utils.fields import UrlsafeTokenField
|
||||||
|
|
||||||
|
|
||||||
|
class UrlsaftTokenFieldTests(TestCase):
|
||||||
|
def test_editable_default(self):
|
||||||
|
field = UrlsafeTokenField()
|
||||||
|
self.assertFalse(field.editable)
|
||||||
|
|
||||||
|
def test_editable(self):
|
||||||
|
field = UrlsafeTokenField(editable=True)
|
||||||
|
self.assertTrue(field.editable)
|
||||||
|
|
||||||
|
def test_max_length_default(self):
|
||||||
|
field = UrlsafeTokenField()
|
||||||
|
self.assertEqual(field.max_length, 128)
|
||||||
|
|
||||||
|
def test_max_length(self):
|
||||||
|
field = UrlsafeTokenField(max_length=256)
|
||||||
|
self.assertEqual(field.max_length, 256)
|
||||||
|
|
||||||
|
def test_factory_default(self):
|
||||||
|
field = UrlsafeTokenField()
|
||||||
|
self.assertIsNone(field._factory)
|
||||||
|
|
||||||
|
def test_factory_not_callable(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
UrlsafeTokenField(factory='INVALID')
|
||||||
|
|
||||||
|
def test_get_default(self):
|
||||||
|
field = UrlsafeTokenField()
|
||||||
|
value = field.get_default()
|
||||||
|
self.assertEqual(len(value), field.max_length)
|
||||||
|
|
||||||
|
def test_get_default_with_non_default_max_length(self):
|
||||||
|
field = UrlsafeTokenField(max_length=64)
|
||||||
|
value = field.get_default()
|
||||||
|
self.assertEqual(len(value), 64)
|
||||||
|
|
||||||
|
def test_get_default_with_factory(self):
|
||||||
|
token = 'SAMPLE_TOKEN'
|
||||||
|
factory = Mock(return_value=token)
|
||||||
|
field = UrlsafeTokenField(factory=factory)
|
||||||
|
value = field.get_default()
|
||||||
|
|
||||||
|
self.assertEqual(value, token)
|
||||||
|
factory.assert_called_once_with(field.max_length)
|
||||||
|
|
||||||
|
def test_no_default_param(self):
|
||||||
|
field = UrlsafeTokenField(default='DEFAULT')
|
||||||
|
self.assertIs(field.default, NOT_PROVIDED)
|
||||||
Loading…
Reference in a new issue