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>
|
||||
| Éric Araujo <merwok@netwok.org>
|
||||
| Ő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`
|
||||
- Drop support for `Django 3.0`
|
||||
|
||||
- Added urlsafe token field.
|
||||
|
||||
4.1.1 (2020-12-01)
|
||||
------------------
|
||||
- Applied `isort` to codebase (Refs GH-#402)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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