diff --git a/README.md b/README.md index 181305c..8adf24d 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,13 @@ The project was forked from [django-hashids](https://github.com/ericls/django-ha - Proxy the internal model `pk` field without storing the value in the database. - Allows lookups and filtering by sqid string. -- Can be used as sort key -- Allows specifying a min_length and alphabet globally -- Supports custom min_length, and alphabet per field -- Supports Django REST Framework Serializers +- Can be used as sort key. +- Allows specifying a min_length and alphabet globally. +- Supports custom min_length, prefix, and alphabet per field. +- Supports Django REST Framework Serializers. - Supports exact ID searches in Django Admin when field is specified in search_fields. -- Supports common filtering lookups, such as `__iexact`, `__contains`, `__icontains`, though matching is the same as `__exact`. -- Supports other lookups: isnull, gt, gte, lt and lte. +- Supports filtering by `__iexact`, though matching is the same as `__exact`. +- Supports other lookups: `in`, `isnull`, `gt`, `gte`, `lt`, and `lte`. # Install @@ -63,7 +63,6 @@ TestModel.objects.filter(sqid__gt="1Z") # same as id__gt=1, would return instan # Allows usage in queryset.values TestModel.objects.values_list("sqid", flat=True) # ["1Z", "4x"] TestModel.objects.filter(sqid__in=TestModel.objects.values("sqid")) - ``` ## Using with URLs @@ -99,19 +98,20 @@ class MyModelAdmin(admin.ModelAdmin): ## Config -The folloing attributes can be added in settings file to set default arguments of `SqidsField`: +The following attributes can be added in settings file to set default arguments of `SqidsField`: 1. `DJANGO_SQIDS_MIN_LENGTH`: default minimum length 2. `DJANGO_SQIDS_ALPHABET`: default alphabet `SqidsField` does not reqiure any arguments but the following arguments can be supplied to modify its behavior. -| Name | Description | -| ----------------- | :-----------------------------------------------------: | -| `real_field_name` | The proxied field name | -| `sqids_instance` | The sqids instance used to encode/decode for this field | -| `min_length` | The minimum length of sqids generated for this field | -| `alphabet` | The alphabet used by this field to generate sqids | +| Name | Description | Example | +| ----------------- | :-----------------------------------------------------: | ----------------------------------------------------------- | +| `real_field_name` | The proxied field name | sqid = SqidsField(real_field_name="id") | +| `sqids_instance` | The sqids instance used to encode/decode for this field | sqid = SqidsField(sqids_instance=sqids_instance) | +| `min_length` | The minimum length of sqids generated for this field | sqid = SqidsField(min_length=10) | +| `alphabet` | The alphabet used by this field to generate sqids | sqid = SqidsField(alphabet="KHE5J3L2M4N6P7Q8R9T0V1W2X3Y4Z") | +| `prefix` | The prefix used by this field to generate sqids | sqid = SqidsField(prefix="item-") | The argument `sqids_instance` is mutually exclusive to `min_length` and `alphabet`. See [sqids-python](https://github.com/sqids/sqids-python) for more info about the arguments. @@ -119,8 +119,12 @@ Some common Model arguments such as `verbose_name` are also supported. ## Where did the Salt go? -[Sqids removed the "salt" parameter](https://sqids.org/faq#salt) to prevent association with security or safety. -`django_sqids` provides a useful `shuffle_alphabet` function that helps reintroduce the same idea: +When the Hashids project transitioned to Sqids, [Sqids removed the "salt" parameter](https://sqids.org/faq#salt) to prevent the appearance that +it provides security or safety. In Sqids, the order of the alphabet affects the generated sqids. `django_sqids` provides a useful `shuffle_alphabet` +function that helps reintroduce the same idea as the "salt" parameter by shuffling the alphabet. This can be used to generate a unique alphabet for each +instance of `SqidsField` to prevent the same id from generating the same sqid across different instances of `SqidsField`. + +The `seed` parameter is used to generate a unique ordering of alphabet for each instance of `SqidsField`. The `alphabet` parameter can be used to specify a custom alphabet. ```python from django_sqids import SqidsField, shuffle_alphabet diff --git a/django_sqids/field.py b/django_sqids/field.py index 09007a3..395d46b 100644 --- a/django_sqids/field.py +++ b/django_sqids/field.py @@ -36,13 +36,15 @@ def __init__( sqids_instance=None, alphabet=None, min_length=None, - **kwargs + prefix="", + **kwargs, ): kwargs.pop("editable", None) super().__init__(*args, editable=False, **kwargs) self.real_field_name = real_field_name self.min_length = min_length self.alphabet = alphabet + self.prefix = prefix self._explicit_sqids_instance = sqids_instance self.sqids_instance = None @@ -93,13 +95,20 @@ def get_sqid_instance(self): return Sqids(min_length=min_length, alphabet=alphabet) def get_prep_value(self, value): + if self.prefix: + if value.startswith(self.prefix): + value = value[len(self.prefix) :] + else: + return None decoded_values = self.sqids_instance.decode(value) if not decoded_values: return None return decoded_values[0] def from_db_value(self, value, expression, connection, *args): - return self.sqids_instance.encode([value]) + # Prepend the prefix when encoding for display + encoded_value = self.sqids_instance.encode([value]) + return f"{self.prefix}{encoded_value}" if encoded_value is not None else None def get_col(self, alias, output_field=None): if output_field is None: @@ -135,7 +144,9 @@ def __get__(self, instance, name=None): if real_value is None: return "" assert isinstance(real_value, int) - return self.sqids_instance.encode([real_value]) + # Prepend the prefix when encoding for display + encoded_value = self.sqids_instance.encode([real_value]) + return f"{self.prefix}{encoded_value}" def __set__(self, instance, value): pass diff --git a/poetry.lock b/poetry.lock index 609f20c..9ff1537 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "asgiref" @@ -118,63 +118,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.1" +version = "7.4.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, - {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, - {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, - {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, - {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, - {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, - {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, - {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, - {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, - {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, - {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, - {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, - {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, - {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, - {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, - {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, - {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, - {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, - {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, - {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, - {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, - {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, + {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, + {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, + {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, + {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, + {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, + {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, + {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, + {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, + {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, + {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, + {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, + {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, + {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, + {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, + {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, + {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, + {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, + {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, + {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, + {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, + {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, + {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, ] [package.dependencies] @@ -185,13 +185,13 @@ toml = ["tomli"] [[package]] name = "django" -version = "4.2.9" +version = "4.2.11" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" files = [ - {file = "Django-4.2.9-py3-none-any.whl", hash = "sha256:2cc2fc7d1708ada170ddd6c99f35cc25db664f165d3794bc7723f46b2f8c8984"}, - {file = "Django-4.2.9.tar.gz", hash = "sha256:12498cc3cb8bc8038539fef9e90e95f507502436c1f0c3a673411324fa675d14"}, + {file = "Django-4.2.11-py3-none-any.whl", hash = "sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3"}, + {file = "Django-4.2.11.tar.gz", hash = "sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4"}, ] [package.dependencies] @@ -206,13 +206,13 @@ bcrypt = ["bcrypt"] [[package]] name = "django" -version = "5.0.1" +version = "5.0.3" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" files = [ - {file = "Django-5.0.1-py3-none-any.whl", hash = "sha256:f47a37a90b9bbe2c8ec360235192c7fddfdc832206fcf618bb849b39256affc1"}, - {file = "Django-5.0.1.tar.gz", hash = "sha256:8c8659665bc6e3a44fefe1ab0a291e5a3fb3979f9a8230be29de975e57e8f854"}, + {file = "Django-5.0.3-py3-none-any.whl", hash = "sha256:5c7d748ad113a81b2d44750ccc41edc14e933f56581683db548c9257e078cc83"}, + {file = "Django-5.0.3.tar.gz", hash = "sha256:5fb37580dcf4a262f9258c1f4373819aacca906431f505e4688e37f3a99195df"}, ] [package.dependencies] @@ -224,6 +224,21 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "djangorestframework" +version = "3.14.0" +description = "Web APIs for Django, made easy." +optional = false +python-versions = ">=3.6" +files = [ + {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"}, + {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"}, +] + +[package.dependencies] +django = ">=3.0" +pytz = "*" + [[package]] name = "exceptiongroup" version = "1.2.0" @@ -325,18 +340,18 @@ files = [ [[package]] name = "platformdirs" -version = "4.1.0" +version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] [[package]] name = "pluggy" @@ -417,13 +432,13 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-django" -version = "4.7.0" +version = "4.8.0" description = "A Django plugin for pytest." optional = false python-versions = ">=3.8" files = [ - {file = "pytest-django-4.7.0.tar.gz", hash = "sha256:92d6fd46b1d79b54fb6b060bbb39428073396cec717d5f2e122a990d4b6aa5e8"}, - {file = "pytest_django-4.7.0-py3-none-any.whl", hash = "sha256:4e1c79d5261ade2dd58d91208017cd8f62cb4710b56e012ecd361d15d5d662a2"}, + {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, + {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, ] [package.dependencies] @@ -433,6 +448,17 @@ pytest = ">=7.0.0" docs = ["sphinx", "sphinx-rtd-theme"] testing = ["Django", "django-configurations (>=2.0)"] +[[package]] +name = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + [[package]] name = "sqids" version = "0.4.1" @@ -473,27 +499,27 @@ files = [ [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] name = "tzdata" -version = "2023.4" +version = "2024.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, - {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4" -content-hash = "99fccf3378e3cdb44adcc01886d16fbb3215dadd68886a751059704b2723cb59" +content-hash = "305e1c3e735252f57fc8f56836873a37c207783792460d6146cc5b9651defff9" diff --git a/pyproject.toml b/pyproject.toml index 9d7d885..cb59c14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,8 +15,8 @@ sqids = ">=0.4.1" [tool.poetry.group.dev.dependencies] django = [ - { version = "^4.2.7", python = ">=3.8.1,<3.10" }, - { version = "^5.0.1", python = ">=3.10" }, + { version = "^4.2.7", python = ">=3.8.1,<3.10" }, + { version = "^5.0.1", python = ">=3.10" }, ] black = "^23.11.0" flake8 = "^6.1.0" @@ -24,6 +24,7 @@ isort = "^5.12.0" pytest = "^7.4.3" pytest-cov = "^4.1.0" pytest-django = "^4.7.0" +djangorestframework = "^3.14.0" [build-system] requires = ["poetry>=0.12"] diff --git a/tests/settings.py b/tests/settings.py index 70d221f..d957058 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -42,3 +42,9 @@ }, } } +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + }, +] diff --git a/tests/test_app/models.py b/tests/test_app/models.py index e5ea88b..19e814d 100644 --- a/tests/test_app/models.py +++ b/tests/test_app/models.py @@ -10,6 +10,10 @@ class TestModel(Model): sqid = SqidsField(real_field_name="id") +class TestModelWithPrefix(Model): + sqid = SqidsField(real_field_name="id", prefix="P-") + + class TestModelWithDifferentConfig(Model): sqid = SqidsField(min_length=5, alphabet="OPQRST1234567890") @@ -25,6 +29,10 @@ class TestUser(AbstractUser): sqid = SqidsField(real_field_name="id") +class TestUserWithPrefix(AbstractUser): + sqid = SqidsField(real_field_name="id", prefix="U-") + + class TestUserRelated(Model): sqid = SqidsField(real_field_name="id") @@ -33,6 +41,14 @@ class TestUserRelated(Model): ) +class TestUserRelatedWithPrefix(Model): + sqid = SqidsField(real_field_name="id", prefix="R-") + + user = models.ForeignKey( + "TestUserWithPrefix", related_name="related", on_delete=models.CASCADE + ) + + class FirstSubClass(TestModel): pass diff --git a/tests/test_app/templates/test_app/testmodel.html b/tests/test_app/templates/test_app/testmodel.html new file mode 100644 index 0000000..8375761 --- /dev/null +++ b/tests/test_app/templates/test_app/testmodel.html @@ -0,0 +1,12 @@ + + + Test Model + + + +
+

Test Model

+
+
Placeholder template for testing urls
+ + diff --git a/tests/test_app/views.py b/tests/test_app/views.py new file mode 100644 index 0000000..6c9e1e1 --- /dev/null +++ b/tests/test_app/views.py @@ -0,0 +1,15 @@ +from django.shortcuts import get_object_or_404, render + +from .models import TestModel, TestModelWithPrefix + + +def test_model_view(request, sqid): + test_model = get_object_or_404(TestModel, sqid=sqid) + return render(request, "test_app/testmodel.html", {"object": test_model}) + + +def test_model_with_prefix_view(request, sqid): + test_model_with_prefix = get_object_or_404(TestModelWithPrefix, sqid=sqid) + return render( + request, "test_app/testmodel.html", {"object": test_model_with_prefix} + ) diff --git a/tests/test_django_sqids.py b/tests/test_django_sqids.py index 038b099..ac2bff2 100644 --- a/tests/test_django_sqids.py +++ b/tests/test_django_sqids.py @@ -4,10 +4,13 @@ from django import setup from django.db.models import ExpressionWrapper, F, IntegerField from django.test import override_settings +from django.urls import reverse +from rest_framework import serializers from sqids import Sqids -from django_sqids.field import shuffle_alphabet +from django_sqids import SqidsField from django_sqids.exceptions import ConfigError, RealFieldDoesNotExistError +from django_sqids.field import shuffle_alphabet os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" setup() @@ -17,6 +20,7 @@ def test_can_get_sqids(): from django.conf import settings + from tests.test_app.models import TestModel instance = TestModel.objects.create() @@ -49,8 +53,9 @@ def test_can_use_per_field_instance(): def test_throws_when_setting_both_instance_and_config(): from django.db.models import Model - from tests.test_app.models import this_sqids_instance + from django_sqids import SqidsField + from tests.test_app.models import this_sqids_instance with pytest.raises(ConfigError): @@ -73,6 +78,7 @@ def test_shuffle_alphabet_uses_alphabet(): def test_updates_when_changing_real_column_value(): from django.conf import settings + from tests.test_app.models import TestModel instance = TestModel.objects.create() @@ -88,6 +94,7 @@ def test_updates_when_changing_real_column_value(): def test_ignores_changes_to_value(): from django.conf import settings + from tests.test_app.models import TestModel instance = TestModel.objects.create() @@ -226,7 +233,7 @@ def test_create_user(): def test_multiple_level_inheritance(): # https://github.com/ericls/django-sqids/issues/25 - from tests.test_app.models import SecondSubClass, FirstSubClass + from tests.test_app.models import FirstSubClass, SecondSubClass instance = SecondSubClass.objects.create() SecondSubClass.objects.filter(id=1).first() == SecondSubClass.objects.filter( @@ -241,7 +248,7 @@ def test_multiple_level_inheritance(): def test_multiple_level_inheritance_from_abstract_model(): # https://github.com/ericls/django-sqids/issues/25 - from tests.test_app.models import ModelB, ModelA + from tests.test_app.models import ModelA, ModelB instance = ModelB.objects.create() ModelB.objects.filter(id=1).first() == ModelB.objects.filter( @@ -277,6 +284,7 @@ def test_using_pk_as_real_field_name(): def test_no_real_field_error_message(): from django.db.models import Model + from django_sqids import SqidsField class Foo(Model): @@ -287,3 +295,191 @@ class Meta: with pytest.raises(RealFieldDoesNotExistError): Foo.objects.filter(hash_id="foo") + + +def test_prefix_is_applied_correctly(): + from tests.test_app.models import TestModelWithPrefix + + instance = TestModelWithPrefix.objects.create() + assert instance.sqid.startswith("P-"), "The sqid field value should start with 'P-'" + + +def test_lookups_work_with_manual_prefix(): + from tests.test_app.models import TestModelWithPrefix + + instance = TestModelWithPrefix.objects.create() + sqids = Sqids() + sqids_with_prefix = f"P-{sqids.encode([instance.pk])}" + + got_instance = TestModelWithPrefix.objects.filter( + sqid__exact=sqids_with_prefix + ).first() + assert instance == got_instance, "Exact lookup with prefix should work" + + +def test_lookups_ignore_prefix(): + from tests.test_app.models import TestModelWithPrefix + + instance = TestModelWithPrefix.objects.create() + fetched_instance = TestModelWithPrefix.objects.get(sqid=instance.sqid) + + assert ( + fetched_instance == instance + ), "Should be able to fetch the instance by sqid even with prefix" + + +def test_prefix_does_not_affect_filtering(): + from tests.test_app.models import TestModelWithPrefix + + instance1 = TestModelWithPrefix.objects.create() + instance2 = TestModelWithPrefix.objects.create() + sqids = [instance1.sqid, instance2.sqid] + + filtered_instances = set(TestModelWithPrefix.objects.filter(sqid__in=sqids)) + assert filtered_instances == { + instance1, + instance2, + }, "Filtering by sqid with prefix should return correct instances" + + +def test_prefix_with_exact_lookup(): + from tests.test_app.models import TestModelWithPrefix + + instance = TestModelWithPrefix.objects.create() + got_instance = TestModelWithPrefix.objects.filter(sqid__exact=instance.sqid).first() + assert instance == got_instance, "Exact lookup with prefix should work" + + +def test_prefix_with_in_lookup(): + from tests.test_app.models import TestModelWithPrefix + + instance1 = TestModelWithPrefix.objects.create() + instance2 = TestModelWithPrefix.objects.create() + sqids_with_prefix = [instance1.sqid, instance2.sqid] + + qs = TestModelWithPrefix.objects.filter(sqid__in=sqids_with_prefix) + assert set([instance1, instance2]) == set( + qs + ), "IN lookup with prefix should return correct instances" + + +def test_lookup_with_incorrect_prefix(): + """Tests behavior when an incorrect prefix is used in a lookup.""" + from tests.test_app.models import TestModelWithPrefix + + instance = TestModelWithPrefix.objects.create() + incorrect_sqid = "X-" + instance.sqid[2:] + with pytest.raises(TestModelWithPrefix.DoesNotExist): + TestModelWithPrefix.objects.get(sqid=incorrect_sqid) + + +def test_case_sensitivity_with_prefix(): + """Tests case sensitivity in lookups involving prefixes.""" + from tests.test_app.models import TestModelWithPrefix + + instance = TestModelWithPrefix.objects.create() + # Use a different case for the prefix in the lookup + mixed_case_sqid = "p-" + instance.sqid[2:].lower() + with pytest.raises(TestModelWithPrefix.DoesNotExist): + TestModelWithPrefix.objects.get(sqid=mixed_case_sqid) + + +def test_complex_query_with_prefix(): + """Tests a complex query (e.g., join) to ensure prefix doesn't interfere.""" + from tests.test_app.models import TestUserRelatedWithPrefix, TestUserWithPrefix + + user = TestUserWithPrefix.objects.create() + related = TestUserRelatedWithPrefix.objects.create(user=user) + + fetched_related = ( + TestUserRelatedWithPrefix.objects.select_related("user") + .filter(user__sqid=user.sqid) + .first() + ) + assert ( + fetched_related == related + ), "Complex query with prefix should return correct related instance" + + +def test_serialization_with_prefix(): + """Test DRF serialization and deserialization with prefix.""" + from tests.test_app.models import TestModelWithPrefix + + class TestModelWithPrefixSerializer(serializers.ModelSerializer): + class Meta: + model = TestModelWithPrefix + fields = ["sqid"] + + instance = TestModelWithPrefix.objects.create() + serializer = TestModelWithPrefixSerializer(instance) + + # Simulate serialization + serialized_data = serializer.data + assert serialized_data["sqid"].startswith( + "P-" + ), "Serialized data should contain prefixed sqid" + + # Simulate deserialization and validation + input_data = {"sqid": serialized_data["sqid"]} + new_serializer = TestModelWithPrefixSerializer(data=input_data) + assert new_serializer.is_valid(), "Deserialized data with prefix should be valid" + + +def test_url_for_model_without_prefix(client): + """Test that the URL for a model without prefix can be resolved.""" + from tests.test_app.models import TestModel + + instance = TestModel.objects.create() + url = reverse("without-prefix", kwargs={"sqid": instance.sqid}) + response = client.get(url) + assert response.status_code == 200 + assert response.context["object"] == instance + + +def test_incorrect_url_for_model_without_prefix(client): + """Tests that url fails when adding a prefix for model not expecting prefix.""" + from tests.test_app.models import TestModel + + instance = TestModel.objects.create() + url = reverse("without-prefix", kwargs={"sqid": "P-" + instance.sqid}) + response = client.get(url) + assert response.status_code == 404, "URL for model not expecting prefix return 404" + + +def test_url_for_model_with_prefix(client): + """Test that the URL for a model with prefix can be resolved.""" + from tests.test_app.models import TestModelWithPrefix + + instance = TestModelWithPrefix.objects.create() + url = reverse("with-prefix", kwargs={"sqid": instance.sqid}) + response = client.get(url) + assert response.status_code == 200 + assert response.context["object"] == instance + + +def test_incortect_url_for_model_with_prefix(client): + """Tests that url fails when resolving URL with incorrect prefix.""" + from tests.test_app.models import TestModelWithPrefix + + instance = TestModelWithPrefix.objects.create() + url = reverse("with-prefix", kwargs={"sqid": instance.sqid[2:]}) + response = client.get(url) + assert response.status_code == 404, "URL without prefix returns 404" + + url = reverse("with-prefix", kwargs={"sqid": f"R-{instance.sqid[2:]}"}) + response = client.get(url) + assert response.status_code == 404, "URL with incorrect prefix returns 404" + + +def test_url_manually_with_prefix(client): + """Test that the URL for a model with prefix can be resolved manually.""" + from tests.test_app.models import TestModelWithPrefix + + instance = TestModelWithPrefix.objects.create() + sqids = Sqids() + sqids_with_prefix = f"P-{sqids.encode([instance.pk])}" + + url = reverse("with-prefix", kwargs={"sqid": sqids_with_prefix}) + response = client.get(url) + assert response.status_code == 200 + assert response.context["object"] == instance diff --git a/tests/urls.py b/tests/urls.py index 637600f..24a4a5c 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1 +1,12 @@ -urlpatterns = [] +from django.urls import path + +from .test_app.views import test_model_view, test_model_with_prefix_view + +urlpatterns = [ + path("without-prefix//", test_model_view, name="without-prefix"), + path( + "with-prefix//", + test_model_with_prefix_view, + name="with-prefix", + ), +]