Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ dependencies = [
"django>=3.1",
]
[project.optional-dependencies]
drf = [
"djangorestframework>=3.12",
]
all = [
"django-json-array-field[drf]",
]
# for test/dev purposes
test = [
"pytest==7.4.3",
Expand All @@ -55,7 +61,7 @@ lint-and-formatting = [
"pre-commit",
"mypy"
]
dev = ["django-json-array-field[test, lint-and-formatting]", "bump2version~=1.0.1"]
dev = ["django-json-array-field[all, test, lint-and-formatting]", "bump2version~=1.0.1"]

[project.urls]
"Homepage" = "https://github.com/ottuco/django-json-array-field"
Expand All @@ -81,11 +87,9 @@ exclude_also = [
"def __str__",
"def __repr__",
"def __bool__",

# Type Checking
"if TYPE_CHECKING:",
"if typing.TYPE_CHECKING:",

# Other
"raise NotImplementedError",
]
Expand Down
21 changes: 10 additions & 11 deletions src/json_array_field/db/fields.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,32 @@
from typing import Any

from django.db.models import JSONField
from django.forms import Field as FormField

from ..forms.fields import JSONArrayFormField
from ..utils import str_to_list
from ..utils import clean_input_data


class JSONArrayField(JSONField):
"""
An alternative solution to `django_mysql.models.List[Char|Text]Field`
"""

def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs) -> None:
kwargs.setdefault("default", list)
self.validate_default_type(default=kwargs["default"])
super().__init__(*args, **kwargs)

def validate_default_type(self, default):
def validate_default_type(self, default: Any) -> None:
if callable(default):
default = default()
if not isinstance(default, list):
raise TypeError("Default value must be a list")

def clean_input(self, value):
if not value:
return self.get_default() or []
if isinstance(value, str):
return str_to_list(value=value)
return value
def clean_input(self, value: Any) -> list:
return clean_input_data(data=value, default=self.get_default)

def pre_save(self, model_instance, add):
def pre_save(self, model_instance, add) -> list:
"""
The `to_python(...)` method doesn't get called when we assign values directly to
the field. But, we really need to convert the strings into arrays.
Expand Down Expand Up @@ -65,7 +64,7 @@ class Person(models.Model):

return value

def formfield(self, **kwargs):
def formfield(self, **kwargs) -> FormField:
defaults = {
"form_class": JSONArrayFormField,
}
Expand Down
2 changes: 1 addition & 1 deletion src/json_array_field/forms/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@


class JSONArrayFormField(forms.CharField):
def clean(self, value):
def clean(self, value: str) -> list:
value = super().clean(value)
return str_to_list(value=value)
Empty file.
11 changes: 11 additions & 0 deletions src/json_array_field/rest_framework/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from rest_framework.fields import Field

from ..utils import clean_input_data


class JSONArrayField(Field):
def to_internal_value(self, data):
return clean_input_data(data, self.default)

def to_representation(self, value):
return value
21 changes: 21 additions & 0 deletions src/json_array_field/utils.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,23 @@
def str_to_list(value: str) -> list:
return list(filter(None, map(str.strip, value.split(","))))


def clean_input_data(data, default=None):
if not data:
# data is either blank or None
#
# If data is blank, then we should return an empty list.
if default is None:
return []

# Check whether the `default` is callable or not.
# If it's callable, then call it and return the result.
if callable(default):
return default()
return default

if isinstance(data, str):
return str_to_list(value=data)
if isinstance(data, list):
return data
raise TypeError("Invalid type of data. Expected a string or list.")
56 changes: 56 additions & 0 deletions tests/params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
params_form = [
(
# Single value
"Admin",
["Admin"],
),
(
# Normal
"Admin, Editor, Author",
["Admin", "Editor", "Author"],
),
(
# Whitespace
"Admin, Editor,Author, ",
["Admin", "Editor", "Author"],
),
(
# With double-quotes
'"Admin", "Editor", "Author"',
['"Admin"', '"Editor"', '"Author"'],
),
(
# With single-quotes
"'Admin', 'Editor', 'Author'",
["'Admin'", "'Editor'", "'Author'"],
),
(
# with string-list representation
'["Admin", "Editor", "Author"]',
['["Admin"', '"Editor"', '"Author"]'],
),
]

params = [
*params_form,
# Additional params that can be used directly with the model or via APIs
(
# with python-list
["Admin", "Editor", "Author"],
["Admin", "Editor", "Author"],
),
]

params_with_null_and_blank = [
*params,
(
# Null
None,
[],
),
(
# empty
[],
[],
),
]
21 changes: 0 additions & 21 deletions tests/polls/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,3 @@ class JSONArrayFieldModel(models.Model):

def __str__(self):
return str(self.list_field)


# class JSONArrayWithDefault(models.Model):
# list_field = JSONArrayField(default=list)
#
# def __str__(self):
# return str(self.list_field)
#
#
# class JSONArrayBlank(models.Model):
# list_field = JSONArrayField(blank=True)
#
# def __str__(self):
# return str(self.list_field)
#
#
# class JSONArrayNull(models.Model):
# list_field = JSONArrayField(null=True)
#
# def __str__(self):
# return str(self.list_field)
14 changes: 14 additions & 0 deletions tests/polls/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from rest_framework import serializers

from json_array_field.rest_framework.fields import JSONArrayField
from tests.polls.models import JSONArrayFieldModel


class JSONArraySerializer(serializers.Serializer):
list_field = JSONArrayField()


class JSONArrayModelSerializer(serializers.ModelSerializer):
class Meta:
model = JSONArrayFieldModel
fields = "__all__"
50 changes: 4 additions & 46 deletions tests/test_json_array_field/test_db_fields.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,14 @@
import pytest

from json_array_field.db.fields import JSONArrayField
from tests.params import params_with_null_and_blank
from tests.polls.models import JSONArrayFieldModel

pytestmark = pytest.mark.django_db

params = [
(
# Normal
"Admin, Editor, Author",
["Admin", "Editor", "Author"],
),
(
# Whitespace
"Admin, Editor,Author, ",
["Admin", "Editor", "Author"],
),
(
# With double-quotes
'"Admin", "Editor", "Author"',
['"Admin"', '"Editor"', '"Author"'],
),
(
# With single-quotes
"'Admin', 'Editor', 'Author'",
["'Admin'", "'Editor'", "'Author'"],
),
(
# with string-list representation
'["Admin", "Editor", "Author"]',
['["Admin"', '"Editor"', '"Author"]'],
),
(
# with python-list
["Admin", "Editor", "Author"],
["Admin", "Editor", "Author"],
),
(
# Null
None,
[],
),
(
# empty
[],
[],
),
]


class TestJSONArrayFieldModel:
@pytest.mark.parametrize("list_field, expected_out", params)
@pytest.mark.parametrize("list_field, expected_out", params_with_null_and_blank)
def test_create_using_manager(self, list_field, expected_out):
instance = JSONArrayFieldModel.objects.create(list_field=list_field)

Expand All @@ -60,7 +18,7 @@ def test_create_using_manager(self, list_field, expected_out):
instance = JSONArrayFieldModel.objects.get(pk=instance.pk)
assert instance.list_field == expected_out

@pytest.mark.parametrize("list_field, expected_out", params)
@pytest.mark.parametrize("list_field, expected_out", params_with_null_and_blank)
def test_create_using_constructor(self, list_field, expected_out):
instance = JSONArrayFieldModel(list_field=list_field)
instance.save()
Expand All @@ -71,7 +29,7 @@ def test_create_using_constructor(self, list_field, expected_out):
instance = JSONArrayFieldModel.objects.get(pk=instance.pk)
assert instance.list_field == expected_out

@pytest.mark.parametrize("list_field, expected_out", params)
@pytest.mark.parametrize("list_field, expected_out", params_with_null_and_blank)
def test_assigning_value_using_constructor(self, list_field, expected_out):
instance = JSONArrayFieldModel()
instance.list_field = list_field
Expand Down
35 changes: 4 additions & 31 deletions tests/test_json_array_field/test_form_fields.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,21 @@
import pytest

from tests.params import params_form
from tests.polls.forms import JSONArrayForm, JSONArrayModelForm
from tests.polls.models import JSONArrayFieldModel

pytestmark = pytest.mark.django_db

params = [
(
# Normal
"Admin, Editor, Author",
["Admin", "Editor", "Author"],
),
(
# Whitespace
"Admin, Editor,Author, ",
["Admin", "Editor", "Author"],
),
(
# With double-quotes
'"Admin", "Editor", "Author"',
['"Admin"', '"Editor"', '"Author"'],
),
(
# With single-quotes
"'Admin', 'Editor', 'Author'",
["'Admin'", "'Editor'", "'Author'"],
),
(
# with string-list representation
'["Admin", "Editor", "Author"]',
['["Admin"', '"Editor"', '"Author"]'],
),
]


class TestJSONArrayModelForm:
@pytest.mark.parametrize("list_field, expected_out", params)
@pytest.mark.parametrize("list_field, expected_out", params_form)
def test_create(self, list_field, expected_out):
form = JSONArrayModelForm(data={"list_field": list_field})
assert form.is_valid()
obj = form.save()
assert obj.list_field == expected_out

@pytest.mark.parametrize("list_field, expected_out", params)
@pytest.mark.parametrize("list_field, expected_out", params_form)
def test_update(self, list_field, expected_out):
obj = JSONArrayFieldModel.objects.create(list_field=["foo", "bar"])
assert obj.list_field == ["foo", "bar"]
Expand All @@ -53,7 +26,7 @@ def test_update(self, list_field, expected_out):


class TestJSONArrayForm:
@pytest.mark.parametrize("list_field, expected_out", params)
@pytest.mark.parametrize("list_field, expected_out", params_form)
def test_create_cleaned_data(self, list_field, expected_out):
form = JSONArrayForm(data={"list_field": list_field})
assert form.is_valid()
Expand Down
21 changes: 21 additions & 0 deletions tests/test_json_array_field/test_queries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import pytest

from tests.polls.models import JSONArrayFieldModel

pytestmark = pytest.mark.django_db


class TestDBQueries:
def test_icontains(self):
data = [
"Admin, Editor, Author",
["Administrator", "Editorial", "Authorized"],
]
for list_field in data:
JSONArrayFieldModel.objects.create(list_field=list_field)

# Pull data from the database
qs = JSONArrayFieldModel.objects.filter(list_field__icontains="Editor")
assert qs.count() == 2
qs = JSONArrayFieldModel.objects.filter(list_field__icontains="Authori")
assert qs.count() == 1
Loading