Skip to content

Commit

Permalink
Add tests for IntermediateManyToMany field
Browse files Browse the repository at this point in the history
  • Loading branch information
Eg0ra committed Oct 4, 2024
1 parent 775eae6 commit bd0a8ec
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 6 deletions.
16 changes: 15 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ pytest-django = "^4.9.0"
# This plugin provides a mocker fixture for pytest
# https://pypi.org/project/pytest-mock/
pytest-mock = "^3.14.0"
# Allows you to use fixtures in @pytest.mark.parametrize.
# https://pypi.org/project/pytest-lazy-fixtures/
pytest-lazy-fixtures = "^1.1.1"
# Package for generating test data
# https://factoryboy.readthedocs.io/en/stable/
factory-boy = "^3.3.1"
Expand Down
21 changes: 21 additions & 0 deletions tests/fake_app/migrations/0002_alter_artist_bands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 5.1.1 on 2024-10-02 07:26

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("fake_app", "0001_initial"),
]

operations = [
migrations.AlterField(
model_name="artist",
name="bands",
field=models.ManyToManyField(
related_name="artists",
through="fake_app.Membership",
to="fake_app.band",
),
),
]
6 changes: 5 additions & 1 deletion tests/fake_app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ class Artist(models.Model):
"""Model representing artist."""

name = models.CharField(max_length=100, unique=True)
bands = models.ManyToManyField("Band", through="Membership")
bands = models.ManyToManyField(
"Band",
through="Membership",
related_name="artists",
)

instrument = models.ForeignKey(
Instrument,
Expand Down
27 changes: 27 additions & 0 deletions tests/fake_app/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,30 @@ def get_queryset(self):
"membership_set__band",
"bands",
)


class BandResourceWithM2M(CeleryModelResource):
"""Band resource with Many2Many field."""

artists = IntermediateManyToManyField(
attribute="artists",
column_name="Artists in band",
widget=IntermediateManyToManyWidget(
rem_model=Artist,
rem_field="name",
extra_fields=["date_joined"],
instance_separator=";",
),
)

class Meta:
model = Band
clean_model_instances = True
fields = ["id", "title", "artists"]

def get_queryset(self):
"""Return a queryset."""
return Band.objects.all().prefetch_related(
"membership_set__artist",
"artists",
)
122 changes: 118 additions & 4 deletions tests/test_models/test_fields.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
from pytest_mock import MockerFixture
from django.db import models

import pytest
import pytest_mock
from pytest_lazy_fixtures import lf

from import_export_extensions.fields import IntermediateManyToManyField

from ..fake_app import factories
from ..fake_app.models import Artist, Membership
from ..fake_app.models import Artist, Band, Instrument, Membership


def test_save_method(existing_artist: Artist, mocker: MockerFixture):
def test_save_method(
existing_artist: Artist,
mocker: pytest_mock.MockerFixture,
):
"""Ensure that ``save`` method works properly.
Logic should be the following:
Expand Down Expand Up @@ -45,10 +52,50 @@ def test_save_method(existing_artist: Artist, mocker: MockerFixture):
assert existing_artist.bands.count() == 2


def test_save_readonly_field(existing_artist: Artist):
def test_save_readonly_field(existing_artist: Artist, band: Band):
"""Simple test to check that readonly field changes nothing."""
factories.MembershipFactory(artist=existing_artist, band=band)
field = IntermediateManyToManyField(attribute="bands", readonly=True)
field.save(existing_artist, {})
assert existing_artist.bands.all()


def test_save_with_exception_during_full_clean(
existing_artist: Artist,
band: Band,
mocker: pytest_mock.MockerFixture,
):
"""Check that the exception was handled during full_clean."""
wrong_format_data = "wrong_date_format"
column_name = "Bands"

# suggest widget returned following data
mocker.patch(
target=(
"import_export_extensions.fields.IntermediateManyToManyField.clean"
),
return_value=[
{
"object": band,
"properties": {
"date_joined": wrong_format_data,
},
},
],
)

intermediate_field = IntermediateManyToManyField(
attribute="bands",
column_name=column_name,
)
with pytest.raises(
ValueError,
match=(
f"Column '{column_name}':.*{wrong_format_data}.*value has an "
"invalid date format. It must be in YYYY-MM-DD format."
),
):
intermediate_field.save(existing_artist, {})


def test_get_value(existing_artist: Artist):
Expand All @@ -71,3 +118,70 @@ def test_get_value_none_attribute(existing_artist: Artist):
"""There should be no error if ``attribute`` not set."""
field = IntermediateManyToManyField()
assert field.get_value(existing_artist) is None


@pytest.mark.parametrize(
argnames=["obj", "attribute", "expected_field_params"],
argvalues=[
pytest.param(
lf("existing_artist"),
"bands",
("artist", "band"),
id="Object with forward relation",
),
pytest.param(
lf("band"),
"artists",
("band", "artist"),
id="Object with reversed relation",
),
],
)
def test_get_relation_field_params(
obj: models.Model,
attribute: str,
expected_field_params: tuple[str, str],
):
"""Test that method returns correct relation field params."""
intermediate_field = IntermediateManyToManyField(attribute)
m2m_rel, field_name, reversed_field_name = (
intermediate_field.get_relation_field_params(obj)
)
assert m2m_rel.through == Membership
assert (field_name, reversed_field_name) == expected_field_params


def test_get_through_model_accessor_name(existing_artist: Artist):
"""Test that method returns correct accessor_name."""
expected_accessor_name = "membership_set"
attribute = "bands"
intermediate_field = IntermediateManyToManyField(attribute)
accessor_name = intermediate_field.get_through_model_accessor_name(
existing_artist,
existing_artist._meta.get_field(attribute).remote_field,
)
assert accessor_name == expected_accessor_name


def test_get_through_model_accessor_name_with_wrong_relation(
existing_artist: Artist,
mocker: pytest_mock.MockerFixture,
):
"""Check that method raise error if m2m relation does not exists."""
attribute = "bands"
m2m_relation = existing_artist._meta.get_field(attribute).remote_field
mocker.patch.object(
target=m2m_relation,
attribute="through",
new=Instrument,
)

intermediate_field = IntermediateManyToManyField(attribute)
with pytest.raises(
ValueError,
match=f"{Artist} has no relation with {Instrument}",
):
intermediate_field.get_through_model_accessor_name(
existing_artist,
m2m_relation,
)

0 comments on commit bd0a8ec

Please sign in to comment.