mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Fixed incorrect display of compound primary keys with foreign key references
Closes #319
This commit is contained in:
parent
3683a6b626
commit
3b53eea382
5 changed files with 122 additions and 10 deletions
|
|
@ -70,8 +70,10 @@ def path_from_row_pks(row, pks, use_rowid, quote=True):
|
||||||
if use_rowid:
|
if use_rowid:
|
||||||
bits = [row['rowid']]
|
bits = [row['rowid']]
|
||||||
else:
|
else:
|
||||||
bits = [row[pk] for pk in pks]
|
bits = [
|
||||||
|
row[pk]["value"] if isinstance(row[pk], dict) else row[pk]
|
||||||
|
for pk in pks
|
||||||
|
]
|
||||||
if quote:
|
if quote:
|
||||||
bits = [urllib.parse.quote_plus(str(bit)) for bit in bits]
|
bits = [urllib.parse.quote_plus(str(bit)) for bit in bits]
|
||||||
else:
|
else:
|
||||||
|
|
@ -817,8 +819,10 @@ def path_with_format(request, format, extra_qs=None):
|
||||||
class CustomRow(OrderedDict):
|
class CustomRow(OrderedDict):
|
||||||
# Loose imitation of sqlite3.Row which offers
|
# Loose imitation of sqlite3.Row which offers
|
||||||
# both index-based AND key-based lookups
|
# both index-based AND key-based lookups
|
||||||
def __init__(self, columns):
|
def __init__(self, columns, values=None):
|
||||||
self.columns = columns
|
self.columns = columns
|
||||||
|
if values:
|
||||||
|
self.update(values)
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
if isinstance(key, int):
|
if isinstance(key, int):
|
||||||
|
|
|
||||||
|
|
@ -303,6 +303,10 @@ INSERT INTO units VALUES (1, 1, 100);
|
||||||
INSERT INTO units VALUES (2, 5000, 2500);
|
INSERT INTO units VALUES (2, 5000, 2500);
|
||||||
INSERT INTO units VALUES (3, 100000, 75000);
|
INSERT INTO units VALUES (3, 100000, 75000);
|
||||||
|
|
||||||
|
CREATE TABLE tags (
|
||||||
|
tag TEXT PRIMARY KEY
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE searchable (
|
CREATE TABLE searchable (
|
||||||
pk integer primary key,
|
pk integer primary key,
|
||||||
text1 text,
|
text1 text,
|
||||||
|
|
@ -310,9 +314,25 @@ CREATE TABLE searchable (
|
||||||
[name with . and spaces] text
|
[name with . and spaces] text
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE searchable_tags (
|
||||||
|
searchable_id integer,
|
||||||
|
tag text,
|
||||||
|
PRIMARY KEY (searchable_id, tag),
|
||||||
|
FOREIGN KEY (searchable_id) REFERENCES searchable(pk),
|
||||||
|
FOREIGN KEY (tag) REFERENCES tags(tag)
|
||||||
|
);
|
||||||
|
|
||||||
INSERT INTO searchable VALUES (1, 'barry cat', 'terry dog', 'panther');
|
INSERT INTO searchable VALUES (1, 'barry cat', 'terry dog', 'panther');
|
||||||
INSERT INTO searchable VALUES (2, 'terry dog', 'sara weasel', 'puma');
|
INSERT INTO searchable VALUES (2, 'terry dog', 'sara weasel', 'puma');
|
||||||
|
|
||||||
|
INSERT INTO tags VALUES ("canine");
|
||||||
|
INSERT INTO tags VALUES ("feline");
|
||||||
|
|
||||||
|
INSERT INTO searchable_tags (searchable_id, tag) VALUES
|
||||||
|
(1, "feline"),
|
||||||
|
(2, "canine")
|
||||||
|
;
|
||||||
|
|
||||||
CREATE VIRTUAL TABLE "searchable_fts"
|
CREATE VIRTUAL TABLE "searchable_fts"
|
||||||
USING FTS3 (text1, text2, [name with . and spaces], content="searchable");
|
USING FTS3 (text1, text2, [name with . and spaces], content="searchable");
|
||||||
INSERT INTO "searchable_fts" (rowid, text1, text2, [name with . and spaces])
|
INSERT INTO "searchable_fts" (rowid, text1, text2, [name with . and spaces])
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ def test_homepage(app_client):
|
||||||
assert response.json.keys() == {'fixtures': 0}.keys()
|
assert response.json.keys() == {'fixtures': 0}.keys()
|
||||||
d = response.json['fixtures']
|
d = response.json['fixtures']
|
||||||
assert d['name'] == 'fixtures'
|
assert d['name'] == 'fixtures'
|
||||||
assert d['tables_count'] == 17
|
assert d['tables_count'] == 19
|
||||||
|
|
||||||
|
|
||||||
def test_database_page(app_client):
|
def test_database_page(app_client):
|
||||||
|
|
@ -188,11 +188,38 @@ def test_database_page(app_client):
|
||||||
'columns': ['pk', 'text1', 'text2', 'name with . and spaces'],
|
'columns': ['pk', 'text1', 'text2', 'name with . and spaces'],
|
||||||
'name': 'searchable',
|
'name': 'searchable',
|
||||||
'count': 2,
|
'count': 2,
|
||||||
'foreign_keys': {'incoming': [], 'outgoing': []},
|
'foreign_keys': {'incoming': [{
|
||||||
|
"other_table": "searchable_tags",
|
||||||
|
"column": "pk",
|
||||||
|
"other_column": "searchable_id"
|
||||||
|
}], 'outgoing': []},
|
||||||
'fts_table': 'searchable_fts',
|
'fts_table': 'searchable_fts',
|
||||||
'hidden': False,
|
'hidden': False,
|
||||||
'label_column': None,
|
'label_column': None,
|
||||||
'primary_keys': ['pk'],
|
'primary_keys': ['pk'],
|
||||||
|
}, {
|
||||||
|
"name": "searchable_tags",
|
||||||
|
"columns": ["searchable_id", "tag"],
|
||||||
|
"primary_keys": ["searchable_id", "tag"],
|
||||||
|
"count": 2,
|
||||||
|
"label_column": None,
|
||||||
|
"hidden": False,
|
||||||
|
"fts_table": None,
|
||||||
|
"foreign_keys": {
|
||||||
|
"incoming": [],
|
||||||
|
"outgoing": [
|
||||||
|
{
|
||||||
|
"other_table": "tags",
|
||||||
|
"column": "tag",
|
||||||
|
"other_column": "tag",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"other_table": "searchable",
|
||||||
|
"column": "searchable_id",
|
||||||
|
"other_column": "pk",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'columns': ['group', 'having', 'and'],
|
'columns': ['group', 'having', 'and'],
|
||||||
'name': 'select',
|
'name': 'select',
|
||||||
|
|
@ -251,6 +278,24 @@ def test_database_page(app_client):
|
||||||
'label_column': None,
|
'label_column': None,
|
||||||
'fts_table': None,
|
'fts_table': None,
|
||||||
'primary_keys': ['pk'],
|
'primary_keys': ['pk'],
|
||||||
|
}, {
|
||||||
|
"name": "tags",
|
||||||
|
"columns": ["tag"],
|
||||||
|
"primary_keys": ["tag"],
|
||||||
|
"count": 2,
|
||||||
|
"label_column": None,
|
||||||
|
"hidden": False,
|
||||||
|
"fts_table": None,
|
||||||
|
"foreign_keys": {
|
||||||
|
"incoming": [
|
||||||
|
{
|
||||||
|
"other_table": "searchable_tags",
|
||||||
|
"column": "tag",
|
||||||
|
"other_column": "tag",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outgoing": [],
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'columns': ['pk', 'distance', 'frequency'],
|
'columns': ['pk', 'distance', 'frequency'],
|
||||||
'name': 'units',
|
'name': 'units',
|
||||||
|
|
|
||||||
|
|
@ -550,6 +550,26 @@ def test_row_html_compound_primary_key(app_client):
|
||||||
assert expected == [[str(td) for td in tr.select('td')] for tr in table.select('tbody tr')]
|
assert expected == [[str(td) for td in tr.select('td')] for tr in table.select('tbody tr')]
|
||||||
|
|
||||||
|
|
||||||
|
def test_compound_primary_key_with_foreign_key_references(app_client):
|
||||||
|
# e.g. a many-to-many table with a compound primary key on the two columns
|
||||||
|
response = app_client.get('/fixtures/searchable_tags')
|
||||||
|
assert response.status == 200
|
||||||
|
table = Soup(response.body, 'html.parser').find('table')
|
||||||
|
expected = [
|
||||||
|
[
|
||||||
|
'<td class="col-Link"><a href="/fixtures/searchable_tags/1,feline">1,feline</a></td>',
|
||||||
|
'<td class="col-searchable_id"><a href="/fixtures/searchable/1">1</a>\xa0<em>1</em></td>',
|
||||||
|
'<td class="col-tag"><a href="/fixtures/tags/feline">feline</a></td>',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'<td class="col-Link"><a href="/fixtures/searchable_tags/2,canine">2,canine</a></td>',
|
||||||
|
'<td class="col-searchable_id"><a href="/fixtures/searchable/2">2</a>\xa0<em>2</em></td>',
|
||||||
|
'<td class="col-tag"><a href="/fixtures/tags/canine">canine</a></td>',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
assert expected == [[str(td) for td in tr.select('td')] for tr in table.select('tbody tr')]
|
||||||
|
|
||||||
|
|
||||||
def test_view_html(app_client):
|
def test_view_html(app_client):
|
||||||
response = app_client.get('/fixtures/simple_view')
|
response = app_client.get('/fixtures/simple_view')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
|
||||||
|
|
@ -75,11 +75,34 @@ def test_path_with_replaced_args(path, args, expected):
|
||||||
assert expected == actual
|
assert expected == actual
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('row,pks,expected_path', [
|
@pytest.mark.parametrize(
|
||||||
({'A': 'foo', 'B': 'bar'}, ['A', 'B'], 'foo,bar'),
|
"row,pks,expected_path",
|
||||||
({'A': 'f,o', 'B': 'bar'}, ['A', 'B'], 'f%2Co,bar'),
|
[
|
||||||
({'A': 123}, ['A'], '123'),
|
({"A": "foo", "B": "bar"}, ["A", "B"], "foo,bar"),
|
||||||
])
|
({"A": "f,o", "B": "bar"}, ["A", "B"], "f%2Co,bar"),
|
||||||
|
({"A": 123}, ["A"], "123"),
|
||||||
|
(
|
||||||
|
utils.CustomRow(
|
||||||
|
["searchable_id", "tag"],
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"searchable_id",
|
||||||
|
{"value": 1, "label": "1"},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tag",
|
||||||
|
{
|
||||||
|
"value": "feline",
|
||||||
|
"label": "feline",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
["searchable_id", "tag"],
|
||||||
|
"1,feline",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_path_from_row_pks(row, pks, expected_path):
|
def test_path_from_row_pks(row, pks, expected_path):
|
||||||
actual_path = utils.path_from_row_pks(row, pks, False)
|
actual_path = utils.path_from_row_pks(row, pks, False)
|
||||||
assert expected_path == actual_path
|
assert expected_path == actual_path
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue