Skip to content

Commit ef32cdd

Browse files
committed
Address review feedback
1 parent 7bc42a9 commit ef32cdd

File tree

6 files changed

+173
-119
lines changed

6 files changed

+173
-119
lines changed

django_mongodb_extensions/mql_panel/forms.py

Lines changed: 58 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,47 +9,26 @@
99
from django.utils.translation import gettext_lazy as _
1010
from pymongo import errors as pymongo_errors
1111

12-
from django_mongodb_extensions.mql_panel.utils import (
13-
QueryParts,
12+
from .utils import (
1413
get_max_query_results,
1514
parse_query_args,
1615
)
1716

1817

1918
class MQLBaseForm(SQLSelectForm):
2019
def _execute_operation(self, operation_type, executor_func):
21-
mql_string = ""
22-
try:
23-
parts = self._get_query_parts()
24-
mql_string = parts.mql_string
25-
return executor_func(
26-
parts.db,
27-
parts.collection,
28-
parts.collection_name,
29-
parts.operation,
30-
parts.args_list,
31-
)
32-
except (ValueError, pymongo_errors.PyMongoError) as e:
33-
# ValueError: unsupported operation or unserializable args.
34-
# PyMongoError: any MongoDB driver error during execution.
35-
return self._handle_operation_error(e, mql_string, operation_type)
36-
37-
def _get_query_parts(self):
3820
query_dict = self.cleaned_data["query"]
3921
alias = query_dict.get("alias", "default")
4022
connection = connections[alias]
41-
collection_name, operation, args_list = parse_query_args(query_dict)
4223
db = connection.database
43-
return QueryParts(
44-
query_dict=query_dict,
45-
alias=alias,
46-
mql_string=query_dict.get("mql", ""),
47-
connection=connection,
48-
db=db,
49-
collection=db[collection_name],
50-
collection_name=collection_name,
51-
operation=operation,
52-
args_list=args_list,
24+
collection_name, operation, args_list = parse_query_args(query_dict)
25+
collection = db[collection_name]
26+
return executor_func(
27+
db,
28+
collection,
29+
collection_name,
30+
operation,
31+
args_list,
5332
)
5433

5534
def _handle_operation_error(self, error, mql_string, operation_type="operation"):
@@ -193,34 +172,58 @@ def _execute_query(self, db, collection, collection_name, operation, args_list):
193172
raise ValueError(f"Unsupported read operation: {operation}")
194173
return self.convert_documents_to_table(result_docs)
195174

175+
def _flatten_single_key_dicts(self, obj):
176+
if isinstance(obj, dict):
177+
if len(obj) == 1:
178+
only_value = next(iter(obj.values()))
179+
return self._flatten_single_key_dicts(only_value)
180+
return {
181+
key_name: self._flatten_single_key_dicts(value_item)
182+
for key_name, value_item in obj.items()
183+
}
184+
elif isinstance(obj, list):
185+
return [self._flatten_single_key_dicts(value_item) for value_item in obj]
186+
return obj
187+
196188
def _format_cell_value(self, value):
197-
"""Format a single cell value for table display."""
198-
if value is None:
199-
return {"value": "", "is_json": False}
200-
# Handle primitive types directly without JSON serialization
201-
if isinstance(value, (str, int, float, bool)):
202-
return {"value": str(value), "is_json": False}
203-
# For complex types (ObjectId, datetime, dicts, lists, etc.), use json_util
204-
try:
205-
serialized = json_util.dumps(value)
206-
except (TypeError, AttributeError):
207-
return {"value": str(value), "is_json": False}
208-
try:
209-
parsed = json.loads(serialized)
210-
except json.JSONDecodeError:
211-
return {"value": serialized, "is_json": False}
212-
# Extract value from single-key BSON extended JSON objects like {"$oid": "..."}
213-
if isinstance(parsed, dict) and len(parsed) == 1:
214-
key, val = next(iter(parsed.items()))
215-
return {"value": str(val), "is_json": False}
216-
# For multi-key objects, format with indentation for readability
217-
if isinstance(parsed, dict) and len(parsed) > 1:
218-
return {"value": json.dumps(parsed, indent=4), "is_json": True}
219-
# For lists and other types, use compact serialization
220-
return {"value": serialized, "is_json": False}
189+
serialized = json_util.dumps(value)
190+
parsed_json = json.loads(serialized)
191+
flattened_value = self._flatten_single_key_dicts(parsed_json)
192+
if isinstance(flattened_value, (str, int, float, bool)):
193+
return {"value": str(flattened_value), "is_json": False}
194+
if isinstance(flattened_value, dict):
195+
return {
196+
"type": "dict",
197+
"value": self._format_dict_for_template(flattened_value),
198+
"is_json": False,
199+
}
200+
if isinstance(flattened_value, list):
201+
return {
202+
"type": "list",
203+
"value": self._format_list_for_template(flattened_value),
204+
"is_json": False,
205+
}
206+
return {
207+
"value": json.dumps(flattened_value, indent=4),
208+
"is_json": True,
209+
}
210+
211+
def _format_dict_for_template(self, dictionary):
212+
return [
213+
{"key": key_name, **self._format_cell_value(value_item)}
214+
for key_name, value_item in dictionary.items()
215+
]
216+
217+
def _format_list_for_template(self, list_items):
218+
return [
219+
{"key": index, **self._format_cell_value(value_item)}
220+
for index, value_item in enumerate(list_items)
221+
]
222+
223+
def _format_row(self, row_dict):
224+
return [self._format_cell_value(cell_value) for cell_value in row_dict.values()]
221225

222226
def convert_documents_to_table(self, documents):
223-
"""Convert MongoDB documents to a table of rows and headers."""
224227
if not documents:
225228
return [], []
226229
# Collect all unique field names

django_mongodb_extensions/mql_panel/panel.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
from django.utils.translation import gettext_lazy as _
1515
from django.utils.translation import ngettext
1616

17-
from django_mongodb_extensions.mql_panel import views
18-
from django_mongodb_extensions.mql_panel.utils import (
17+
from . import views
18+
from .utils import (
1919
get_mql_warning_threshold,
2020
patch_get_collection,
2121
patch_new_connection,

django_mongodb_extensions/mql_panel/utils.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,32 +36,6 @@ def log(self, op, duration, args, kwargs=None):
3636
)
3737

3838

39-
class QueryParts:
40-
"""Structured container for parsed query components."""
41-
42-
def __init__(
43-
self,
44-
query_dict,
45-
alias,
46-
mql_string,
47-
connection,
48-
db,
49-
collection,
50-
collection_name,
51-
operation,
52-
args_list,
53-
):
54-
self.query_dict = query_dict
55-
self.alias = alias
56-
self.mql_string = mql_string
57-
self.connection = connection
58-
self.db = db
59-
self.collection = collection
60-
self.collection_name = collection_name
61-
self.operation = operation
62-
self.args_list = args_list
63-
64-
6539
def format_mql_query(query):
6640
"""Return a pretty-printed MQL query string."""
6741
mql_string = query.get("mql", "")

django_mongodb_extensions/templates/mql_panel/mql_query.html

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ <h3>{% translate "Query Results" %}</h3>
1313
<dt>{% translate "Database" %}</dt>
1414
<dd>{{ alias }}</dd>
1515
</dl>
16+
1617
<h4>{% translate "Query Results" %}</h4>
1718
<table>
1819
<thead>
@@ -27,10 +28,57 @@ <h4>{% translate "Query Results" %}</h4>
2728
<tr>
2829
{% for column in row %}
2930
<td>
30-
{% if column.is_json %}
31-
<pre style="margin: 0; padding: 0; border: none; background: none; white-space: pre-wrap; word-wrap: break-word; font-family: inherit;">{{ column.value|escape }}</pre>
31+
{% if column.type == "dict" %}
32+
<table class="subtable">
33+
{% for sub in column.value %}
34+
<tr>
35+
<th>{{ sub.key }}</th>
36+
<td>
37+
{% if sub.type == "dict" %}
38+
<table class="subtable">
39+
{% for inner in sub.value %}
40+
<tr><th>{{ inner.key }}</th><td>{{ inner.value }}</td></tr>
41+
{% endfor %}
42+
</table>
43+
{% elif sub.type == "list" %}
44+
<ul>
45+
{% for inner in sub.value %}
46+
<li>{{ inner.value }}</li>
47+
{% endfor %}
48+
</ul>
49+
{% else %}
50+
{{ sub.value }}
51+
{% endif %}
52+
</td>
53+
</tr>
54+
{% endfor %}
55+
</table>
56+
{% elif column.type == "list" %}
57+
<ul>
58+
{% for sub in column.value %}
59+
<li>
60+
{% if sub.type == "dict" %}
61+
<table class="subtable">
62+
{% for inner in sub.value %}
63+
<tr><th>{{ inner.key }}</th><td>{{ inner.value }}</td></tr>
64+
{% endfor %}
65+
</table>
66+
{% elif sub.type == "list" %}
67+
<ul>
68+
{% for inner in sub.value %}
69+
<li>{{ inner.value }}</li>
70+
{% endfor %}
71+
</ul>
72+
{% else %}
73+
{{ sub.value }}
74+
{% endif %}
75+
</li>
76+
{% endfor %}
77+
</ul>
78+
{% elif column.is_json %}
79+
<pre style="margin: 0; padding: 0; border: none; background: none; white-space: pre-wrap; word-wrap: break-word; font-family: inherit;">{{ column.value }}</pre>
3280
{% else %}
33-
{{ column.value|escape }}
81+
{{ column.value }}
3482
{% endif %}
3583
</td>
3684
{% endfor %}

tests/mql_panel/test_forms.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import datetime
2+
import json
3+
4+
from bson import ObjectId, json_util
15
from django.test import TestCase
26

37
from django_mongodb_extensions.mql_panel.forms import MQLQueryForm
@@ -32,3 +36,61 @@ def test_handle_operation_error_format(self):
3236

3337
# Should have one header
3438
self.assertEqual(headers[0], "Query Parsing Error")
39+
40+
41+
class FormatCellValueTests(TestCase):
42+
def setUp(self):
43+
self.form = MQLQueryForm()
44+
45+
def test_simple_fields(self):
46+
"""Primitive field values like in model query output."""
47+
cases = [
48+
("username", {"value": "username", "is_json": False}),
49+
(42, {"value": "42", "is_json": False}),
50+
(3.14, {"value": "3.14", "is_json": False}),
51+
(True, {"value": "True", "is_json": False}),
52+
]
53+
for input_value, expected in cases:
54+
with self.subTest(value=input_value):
55+
self.assertEqual(self.form._format_cell_value(input_value), expected)
56+
57+
def test_objectid(self):
58+
"""MongoDB ObjectId from a document."""
59+
oid = ObjectId()
60+
result = self.form._format_cell_value(oid)
61+
# ObjectIds serialize to {"$oid": "<id>"} which is single-key
62+
self.assertIs(result["is_json"], False)
63+
self.assertEqual(result["value"], str(json.loads(json_util.dumps(oid))["$oid"]))
64+
65+
def test_datetime(self):
66+
"""Datetime values as they might appear in a MongoDB doc."""
67+
dt = datetime.datetime(2024, 1, 1, 12, 30)
68+
result = self.form._format_cell_value(dt)
69+
# Datetimes serialize to {"$date": timestamp_ms} which is single-key dict
70+
self.assertIs(result["is_json"], False)
71+
self.assertEqual(result["value"], str(json.loads(json_util.dumps(dt))["$date"]))
72+
73+
def test_embedded_document(self):
74+
"""Nested dict with multiple keys (JSON formatting)."""
75+
embedded_doc = {
76+
"name": "Alice",
77+
"age": 30,
78+
"created": datetime.datetime(2024, 2, 1, 18, 0),
79+
}
80+
result = self.form._format_cell_value(embedded_doc)
81+
self.assertIs(result["is_json"], False)
82+
parsed_back = json.loads(result["value"])
83+
# Datetime will still be JSON date dict
84+
self.assertEqual(parsed_back["name"], "Alice")
85+
self.assertEqual(parsed_back["age"], 30)
86+
self.assertIn("$date", parsed_back["created"])
87+
88+
def test_list_field(self):
89+
"""A list of values as might appear in MongoDB array field."""
90+
arr = ["tag1", "tag2", ObjectId()]
91+
result = self.form._format_cell_value(arr)
92+
# Lists are not dicts, so is_json = False
93+
self.assertIs(result["is_json"], False)
94+
self.assertEqual(
95+
result["value"], '["tag1", "tag2", {"$oid": "69cc8219f859271f0a081538"}]'
96+
)

tests/mql_panel/test_utils.py

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)