Added urlsafe token field

This commit is contained in:
Nafees Anwar 2021-03-10 14:06:34 +05:00
parent 57d26ee0cc
commit a56d07cd68
5 changed files with 141 additions and 0 deletions

View file

@ -99,3 +99,4 @@
| zyegfryed <zyegfryed@gmail.com>
| Éric Araujo <merwok@netwok.org>
| Őry Máté <ory.mate@cloud.bme.hu>
| Nafees Anwar <h.nafees.anwar@gmail.com>

View file

@ -6,6 +6,8 @@ Unreleased
- Add support for `Django 3.2`
- Drop support for `Django 3.0`
- Added urlsafe token field.
4.1.1 (2020-12-01)
------------------
- Applied `isort` to codebase (Refs GH-#402)

View file

@ -180,3 +180,46 @@ or any other ModelForm, default is False.
class MyAppModel(models.Model):
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)

View file

@ -1,4 +1,6 @@
import secrets
import uuid
from collections import Callable
from django.conf import settings
from django.core.exceptions import ValidationError
@ -309,3 +311,41 @@ class UUIDField(models.UUIDField):
kwargs.setdefault('editable', editable)
kwargs.setdefault('default', default)
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

View 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)