Skip to content

Commit 40eef2d

Browse files
committed
Address review feedback
1 parent 7bc42a9 commit 40eef2d

File tree

7 files changed

+175
-195
lines changed

7 files changed

+175
-195
lines changed

django_mongodb_extensions/mql_panel/forms.py

Lines changed: 58 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -7,114 +7,29 @@
77
from django.core.exceptions import ValidationError
88
from django.db import connections
99
from django.utils.translation import gettext_lazy as _
10-
from pymongo import errors as pymongo_errors
1110

12-
from django_mongodb_extensions.mql_panel.utils import (
13-
QueryParts,
11+
from .utils import (
1412
get_max_query_results,
1513
parse_query_args,
1614
)
1715

1816

1917
class MQLBaseForm(SQLSelectForm):
2018
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):
3819
query_dict = self.cleaned_data["query"]
3920
alias = query_dict.get("alias", "default")
4021
connection = connections[alias]
41-
collection_name, operation, args_list = parse_query_args(query_dict)
4222
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,
23+
collection_name, operation, args_list = parse_query_args(query_dict)
24+
collection = db[collection_name]
25+
return executor_func(
26+
db,
27+
collection,
28+
collection_name,
29+
operation,
30+
args_list,
5331
)
5432

55-
def _handle_operation_error(self, error, mql_string, operation_type="operation"):
56-
error_map = {
57-
pymongo_errors.OperationFailure: (
58-
"MongoDB Operation Error",
59-
[
60-
f"MongoDB operation failed: {error}",
61-
"The query syntax may be invalid or the operation is not supported.",
62-
],
63-
),
64-
(
65-
pymongo_errors.ConnectionFailure,
66-
pymongo_errors.ServerSelectionTimeoutError,
67-
): (
68-
"MongoDB Connection Error",
69-
[
70-
f"MongoDB connection error: {error}",
71-
"Could not connect to MongoDB server.",
72-
"Check your database connection settings.",
73-
],
74-
),
75-
pymongo_errors.PyMongoError: (
76-
"MongoDB Error",
77-
[
78-
f"MongoDB error: {error}",
79-
"An error occurred while executing the MongoDB operation.",
80-
],
81-
),
82-
}
83-
header, messages = None, []
84-
for err_type, (_header, _messages) in error_map.items():
85-
if isinstance(error, err_type):
86-
header, messages = _header, _messages.copy()
87-
break
88-
if not header:
89-
if isinstance(error, ValueError):
90-
header = "Query Parsing Error"
91-
messages = [f"Query parsing error: {error}"]
92-
if operation_type == "query":
93-
messages += [
94-
"The MQL panel can only re-execute read operations.",
95-
"Write operations (insert, update, delete) cannot be re-executed.",
96-
]
97-
else:
98-
messages += [
99-
"The MQL panel tracks raw MongoDB operations.",
100-
"Some operations may not be re-executable from the debug toolbar.",
101-
]
102-
else:
103-
header = f"{operation_type.capitalize()} Error"
104-
messages = [
105-
f"Unexpected error executing {operation_type}: {error}",
106-
"An unexpected error occurred.",
107-
]
108-
body_text = "\n\n".join(messages)
109-
formatted_body = body_text.split("\n")
110-
formatted_body.extend(["", f"Original query: {mql_string}"])
111-
# Return error in the same format as convert_documents_to_table():
112-
# a one-row table with {value, is_json} cells
113-
error_message = "\n".join(formatted_body)
114-
rows = [[{"value": error_message, "is_json": False}]]
115-
headers = [header]
116-
return rows, headers
117-
11833
def clean(self):
11934
from .panel import MQLPanel
12035

@@ -193,34 +108,58 @@ def _execute_query(self, db, collection, collection_name, operation, args_list):
193108
raise ValueError(f"Unsupported read operation: {operation}")
194109
return self.convert_documents_to_table(result_docs)
195110

111+
def _flatten_single_key_dicts(self, obj):
112+
if isinstance(obj, dict):
113+
if len(obj) == 1:
114+
only_value = next(iter(obj.values()))
115+
return self._flatten_single_key_dicts(only_value)
116+
return {
117+
key_name: self._flatten_single_key_dicts(value_item)
118+
for key_name, value_item in obj.items()
119+
}
120+
elif isinstance(obj, list):
121+
return [self._flatten_single_key_dicts(value_item) for value_item in obj]
122+
return obj
123+
196124
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}
125+
serialized = json_util.dumps(value)
126+
parsed_json = json.loads(serialized)
127+
flattened_value = self._flatten_single_key_dicts(parsed_json)
128+
if isinstance(flattened_value, (str, int, float, bool)):
129+
return {"value": str(flattened_value), "is_json": False}
130+
if isinstance(flattened_value, dict):
131+
return {
132+
"type": "dict",
133+
"value": self._format_dict_for_template(flattened_value),
134+
"is_json": False,
135+
}
136+
if isinstance(flattened_value, list):
137+
return {
138+
"type": "list",
139+
"value": self._format_list_for_template(flattened_value),
140+
"is_json": False,
141+
}
142+
return {
143+
"value": json.dumps(flattened_value, indent=4),
144+
"is_json": True,
145+
}
146+
147+
def _format_dict_for_template(self, dictionary):
148+
return [
149+
{"key": key_name, **self._format_cell_value(value_item)}
150+
for key_name, value_item in dictionary.items()
151+
]
152+
153+
def _format_list_for_template(self, list_items):
154+
return [
155+
{"key": index, **self._format_cell_value(value_item)}
156+
for index, value_item in enumerate(list_items)
157+
]
158+
159+
def _format_row(self, row_dict):
160+
return [self._format_cell_value(cell_value) for cell_value in row_dict.values()]
221161

222162
def convert_documents_to_table(self, documents):
223-
"""Convert MongoDB documents to a table of rows and headers."""
224163
if not documents:
225164
return [], []
226165
# 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: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,39 +36,9 @@ 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."""
67-
mql_string = query.get("mql", "")
68-
try:
69-
collection_name, operation, args_list = parse_query_args(query)
70-
except ValueError:
71-
return mql_string
41+
collection_name, operation, args_list = parse_query_args(query)
7242
if args_list:
7343
args_formatted = json_util.dumps(
7444
args_list[0] if len(args_list) == 1 else args_list,

django_mongodb_extensions/templates/mql_panel/mql_explain.html

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,7 @@ <h3>{% translate "MQL explained" %}</h3>
1717
<h4>{% translate "Explain Output" %}</h4>
1818
{% for row in result %}
1919
{% for item in row %}
20-
{% if item.value %}
21-
{# Handle dict format from _handle_operation_error #}
22-
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">{{ item.value }}</pre>
23-
{% else %}
24-
{# Handle plain string format from _execute_explain #}
25-
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">{{ item }}</pre>
26-
{% endif %}
20+
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">{{ item }}</pre>
2721
{% endfor %}
2822
{% endfor %}
2923
{% else %}

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 %}

0 commit comments

Comments
 (0)