|
7 | 7 | from django.core.exceptions import ValidationError |
8 | 8 | from django.db import connections |
9 | 9 | from django.utils.translation import gettext_lazy as _ |
10 | | -from pymongo import errors as pymongo_errors |
11 | 10 |
|
12 | | -from django_mongodb_extensions.mql_panel.utils import ( |
13 | | - QueryParts, |
| 11 | +from .utils import ( |
14 | 12 | get_max_query_results, |
15 | 13 | parse_query_args, |
16 | 14 | ) |
17 | 15 |
|
18 | 16 |
|
19 | 17 | class MQLBaseForm(SQLSelectForm): |
20 | 18 | 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): |
38 | 19 | query_dict = self.cleaned_data["query"] |
39 | 20 | alias = query_dict.get("alias", "default") |
40 | 21 | connection = connections[alias] |
41 | | - collection_name, operation, args_list = parse_query_args(query_dict) |
42 | 22 | 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, |
53 | 31 | ) |
54 | 32 |
|
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 | | - |
118 | 33 | def clean(self): |
119 | 34 | from .panel import MQLPanel |
120 | 35 |
|
@@ -193,34 +108,58 @@ def _execute_query(self, db, collection, collection_name, operation, args_list): |
193 | 108 | raise ValueError(f"Unsupported read operation: {operation}") |
194 | 109 | return self.convert_documents_to_table(result_docs) |
195 | 110 |
|
| 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 | + |
196 | 124 | 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()] |
221 | 161 |
|
222 | 162 | def convert_documents_to_table(self, documents): |
223 | | - """Convert MongoDB documents to a table of rows and headers.""" |
224 | 163 | if not documents: |
225 | 164 | return [], [] |
226 | 165 | # Collect all unique field names |
|
0 commit comments