Skip to content

Commit 5b1ecae

Browse files
committed
Initial work to support many-to-many fields.
1 parent 11b9360 commit 5b1ecae

File tree

11 files changed

+120
-6
lines changed

11 files changed

+120
-6
lines changed

django_unicorn/serializer.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,20 @@ def _get_model_dict(model: Model) -> dict:
7373
model_json = model_json.get("fields")
7474
model_json["pk"] = model_pk
7575

76+
for field in model._meta.get_fields():
77+
if field.is_relation and field.many_to_many:
78+
related_name = field.related_name or f"{field.name}_set"
79+
pks = []
80+
81+
try:
82+
related_descriptor = getattr(model, related_name)
83+
pks = list(related_descriptor.values_list("pk", flat=True))
84+
except ValueError:
85+
# ValueError is throuwn when the model doesn't have an id already set
86+
pass
87+
88+
model_json[related_name] = pks
89+
7690
return model_json
7791

7892

django_unicorn/views/action_parsers/utils.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,20 @@ class TestView(UnicornView):
6464
is_relation_field = False
6565

6666
# Set the id property for ForeignKeys
67-
# TODO: Move this to utility function
67+
# TODO: Move some of this to utility function
6868
if hasattr(component_or_field, "_meta"):
69-
for field in component_or_field._meta.fields:
70-
if field.name == property_name_part:
69+
for field in component_or_field._meta.get_fields():
70+
if field.is_relation and field.many_to_many:
71+
related_name = field.related_name or f"{field.name}_set"
72+
73+
if related_name == property_name_part:
74+
related_descriptor = getattr(
75+
component_or_field, related_name
76+
)
77+
related_descriptor.set(property_value)
78+
is_relation_field = True
79+
break
80+
elif field.name == property_name_part:
7181
if field.is_relation:
7282
setattr(
7383
component_or_field,

django_unicorn/views/utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ def set_property_from_data(
3434
Sets properties on the component based on passed-in data.
3535
"""
3636

37-
if not hasattr(component_or_field, name):
37+
try:
38+
if not hasattr(component_or_field, name):
39+
return
40+
except ValueError:
41+
# Treat ValueError the same as a missing field because trying to access a many-to-many
42+
# field before the model's pk will throw this exception
3843
return
3944

4045
field = getattr(component_or_field, name)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 3.1.7 on 2021-04-11 18:00
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('coffee', '0003_auto_20210128_0140'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='Taste',
15+
fields=[
16+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17+
('name', models.CharField(max_length=255)),
18+
('flavor', models.ManyToManyField(to='coffee.Flavor')),
19+
],
20+
),
21+
migrations.CreateModel(
22+
name='Origin',
23+
fields=[
24+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
25+
('name', models.CharField(max_length=255)),
26+
('flavor', models.ManyToManyField(related_name='origins', to='coffee.Flavor')),
27+
],
28+
),
29+
]

example/coffee/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,13 @@ class Flavor(models.Model):
1919

2020
def __str__(self):
2121
return self.name
22+
23+
24+
class Taste(models.Model):
25+
name = models.CharField(max_length=255)
26+
flavor = models.ManyToManyField(Flavor)
27+
28+
29+
class Origin(models.Model):
30+
name = models.CharField(max_length=255)
31+
flavor = models.ManyToManyField(Flavor, related_name="origins")

example/unicorn/components/models.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django_unicorn.components import QuerySetType, UnicornView
2-
from example.coffee.models import Flavor
2+
from example.coffee.models import Flavor, Taste
33

44

55
class ModelsView(UnicornView):
@@ -28,5 +28,11 @@ def delete(self, flavor_to_delete: Flavor):
2828
def available_flavors(self):
2929
return Flavor.objects.all()
3030

31+
def available_tastes(self):
32+
return Taste.objects.all()
33+
3134
class Meta:
32-
javascript_exclude = ("available_flavors",)
35+
javascript_exclude = (
36+
"available_flavors",
37+
"available_tastes",
38+
)

example/unicorn/templates/unicorn/models.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ <h4>{{ flavor.pk }}</h4>
5454
{{ flavor.parent.name }}
5555
<br />
5656

57+
<label>Taste</label>
58+
<select unicorn:model="flavors.{{ forloop.counter0 }}.taste_set" multiple>
59+
{% for available_taste in available_tastes %}
60+
<option value="{{ available_taste.pk }}">{{ available_taste.name }}</option>
61+
{% endfor %}
62+
</select>
63+
64+
<ul>
65+
{% for taste in flavor.taste_set.all %}
66+
<li>{{ taste.name }}</li>
67+
{% endfor %}
68+
</ul>
69+
<br />
70+
5771
<button unicorn:click="save({{ forloop.counter0 }})">save(flavor_idx)</button>
5872
<button unicorn:click="delete({{ flavor.pk }})">delete(flavor.pk)</button>
5973
</div>

tests/serializer/test_dumps.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ def test_model_with_datetime(db):
8080
"time": None,
8181
"duration": None,
8282
"pk": None,
83+
"taste_set": [],
84+
"origins": [],
8385
}
8486
}
8587

@@ -104,6 +106,8 @@ def test_model_with_datetime_as_string(db):
104106
"time": None,
105107
"duration": None,
106108
"pk": None,
109+
"taste_set": [],
110+
"origins": [],
107111
}
108112
}
109113

@@ -128,6 +132,8 @@ def test_model_with_time_as_string(db):
128132
"time": time,
129133
"duration": None,
130134
"pk": None,
135+
"taste_set": [],
136+
"origins": [],
131137
}
132138
}
133139

@@ -152,6 +158,8 @@ def test_model_with_duration_as_string(db):
152158
"time": None,
153159
"duration": "-1 19:00:00",
154160
"pk": None,
161+
"taste_set": [],
162+
"origins": [],
155163
}
156164
}
157165

@@ -204,6 +212,8 @@ def test_dumps_queryset(db):
204212
"time": None,
205213
"duration": None,
206214
"pk": 1,
215+
"taste_set": [],
216+
"origins": [],
207217
},
208218
{
209219
"name": "name2",
@@ -217,6 +227,8 @@ def test_dumps_queryset(db):
217227
"time": None,
218228
"duration": None,
219229
"pk": 2,
230+
"taste_set": [],
231+
"origins": [],
220232
},
221233
]
222234
}
@@ -241,6 +253,8 @@ def test_get_model_dict():
241253
"datetime": None,
242254
"time": None,
243255
"duration": None,
256+
"taste_set": [],
257+
"origins": [],
244258
}
245259

246260
assert expected == actual

tests/serializer/test_model_value.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from django.db import models
22

3+
import pytest
4+
35
from django_unicorn.components import ModelValueMixin
46
from django_unicorn.serializer import model_value
57
from example.coffee.models import Flavor
@@ -20,6 +22,8 @@ def test_model_value_all_fields():
2022
"pk": None,
2123
"time": None,
2224
"uuid": str(flavor.uuid),
25+
"taste_set": [],
26+
"origins": [],
2327
}
2428

2529
actual = model_value(flavor)
@@ -36,6 +40,7 @@ def test_model_value_one_field():
3640
assert expected == actual
3741

3842

43+
@pytest.mark.django_db
3944
def test_model_value_multiple_field():
4045
expected = {
4146
"pk": 77,

tests/templatetags/test_unicorn_render.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from django.template.base import Token, TokenType
44

5+
import pytest
6+
57
from django_unicorn.components import UnicornView
68
from django_unicorn.templatetags.unicorn import unicorn
79
from django_unicorn.utils import generate_checksum
@@ -163,6 +165,7 @@ def to_json(self):
163165
)
164166

165167

168+
@pytest.mark.django_db
166169
def test_unicorn_render_parent_with_model_pk(settings):
167170
settings.DEBUG = True
168171
token = Token(

0 commit comments

Comments
 (0)