Skip to content

Commit f23624b

Browse files
committed
Add support for calling model by app label.
1 parent eea8b4e commit f23624b

2 files changed

Lines changed: 107 additions & 2 deletions

File tree

src/dj_angles/templatetags/model.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,22 @@ def get_models() -> dict:
1717
models = {}
1818

1919
for app_config in apps.get_app_configs():
20+
app_label = app_config.label
21+
22+
if app_label not in models:
23+
models[app_label] = {}
24+
2025
for model in app_config.get_models():
21-
# TODO: What if model names overlap?
22-
models[model.__name__] = model
26+
model_name = model.__name__
27+
28+
if model_name in models:
29+
if isinstance(models[model_name], dict):
30+
logger.warning("Model name collision with app label: %s", model_name)
31+
else:
32+
logger.warning("Model name collision: %s. Using %s.", model_name, app_label)
33+
34+
models[model_name] = model
35+
models[app_label][model_name] = model
2336

2437
return models
2538

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from unittest.mock import MagicMock, patch
2+
3+
import pytest
4+
from django.template import Context, Template
5+
6+
from dj_angles.templatetags.model import get_models
7+
8+
9+
# Mock setup for apps and models
10+
class MockModel:
11+
def __init__(self, name):
12+
self.__name__ = name
13+
14+
def __repr__(self):
15+
return f"<Model: {self.__name__}>"
16+
17+
18+
class MockAppConfig:
19+
def __init__(self, label, models):
20+
self.label = label
21+
self._models = models
22+
23+
def get_models(self):
24+
return self._models
25+
26+
27+
@pytest.fixture
28+
def mock_apps():
29+
with patch("dj_angles.templatetags.model.apps") as mock_apps:
30+
yield mock_apps
31+
32+
33+
def test_get_models_structure(mock_apps):
34+
# Setup: 2 apps, "shop" and "inventory"
35+
# "shop" has "Product"
36+
# "inventory" has "Product" (overlap) and "Item"
37+
38+
product1 = MockModel("Product")
39+
product2 = MockModel("Product") # Different object, same name
40+
item = MockModel("Item")
41+
42+
app1 = MockAppConfig("shop", [product1])
43+
app2 = MockAppConfig("inventory", [product2, item])
44+
45+
mock_apps.get_app_configs.return_value = [app1, app2]
46+
47+
models = get_models()
48+
49+
# Check nesting
50+
assert models["shop"]["Product"] is product1
51+
assert models["inventory"]["Product"] is product2
52+
assert models["inventory"]["Item"] is item
53+
54+
# Check short names
55+
assert models["Item"] is item
56+
# Last wins for overlapping short names
57+
assert models["Product"] is product2
58+
59+
60+
def test_model_app_collision_warning(mock_apps, caplog):
61+
# Setup: App named "User" and Model named "User" (in another app)
62+
user_model = MockModel("User")
63+
app_user = MockAppConfig("User", []) # App named User
64+
app_auth = MockAppConfig("auth", [user_model]) # Model named User
65+
66+
mock_apps.get_app_configs.return_value = [app_user, app_auth]
67+
68+
with caplog.at_level("WARNING"):
69+
models = get_models()
70+
71+
# App dict exists first
72+
assert isinstance(models["User"], MockModel) # Overwritten by model
73+
assert models["User"] is user_model
74+
75+
# Verify warning
76+
assert "Model name collision with app label: User" in caplog.text
77+
78+
79+
def test_model_model_collision_warning(mock_apps, caplog):
80+
product1 = MockModel("Product")
81+
product2 = MockModel("Product")
82+
83+
app1 = MockAppConfig("shop", [product1])
84+
app2 = MockAppConfig("inventory", [product2])
85+
86+
mock_apps.get_app_configs.return_value = [app1, app2]
87+
88+
with caplog.at_level("WARNING"):
89+
models = get_models()
90+
91+
assert "Model name collision: Product" in caplog.text
92+
assert "Using inventory" in caplog.text # Should mention the new app label

0 commit comments

Comments
 (0)