def content(self): """Content of the panel when it's displayed in full screen. Fetch every store for the toolbar and include it in the template. """ stores = OrderedDict() for id, toolbar in reversed(self.toolbar._store.items()): stores[id] = { "toolbar": toolbar, "form": SignedDataForm(initial=HistoryStoreForm(initial={ "store_id": id }).initial), } return render_to_string( self.template, { "current_store_id": self.toolbar.store_id, "stores": stores, "refresh_form": SignedDataForm(initial=HistoryStoreForm( initial={ "store_id": self.toolbar.store_id }).initial), }, )
def inner(request, *args, **kwargs): from debug_toolbar.forms import SignedDataForm data = request.GET if request.method == "GET" else request.POST signed_form = SignedDataForm(data) if signed_form.is_valid(): return view(request, *args, verified_data=signed_form.verified_data(), **kwargs) return HttpResponseBadRequest("Invalid signature")
def test_verified_data(self): form = SignedDataForm(data={"signed": SignedDataForm.sign(DATA)}) self.assertEqual( form.verified_data(), { "value": "foo", "date": "2020-01-01 00:00:00+00:00", }, ) # Take it back to the foo form to validate the datetime is serialized foo_form = FooForm(data=form.verified_data()) self.assertTrue(foo_form.is_valid()) self.assertDictEqual(foo_form.cleaned_data, DATA)
def test_history_sidebar_invalid(self): response = self.client.get(reverse("djdt:history_sidebar")) self.assertEqual(response.status_code, 400) data = {"signed": SignedDataForm.sign({"store_id": "foo"}) + "invalid"} response = self.client.get(reverse("djdt:history_sidebar"), data=data) self.assertEqual(response.status_code, 400)
def test_sql_profile_checks_show_toolbar(self): url = "/__debug__/sql_profile/" data = { "signed": SignedDataForm.sign({ "sql": "SELECT * FROM auth_user", "raw_sql": "SELECT * FROM auth_user", "params": "{}", "alias": "default", "duration": "0", }) } response = self.client.post(url, data) self.assertEqual(response.status_code, 200) response = self.client.post(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest") self.assertEqual(response.status_code, 200) with self.settings(INTERNAL_IPS=[]): response = self.client.post(url, data) self.assertEqual(response.status_code, 404) response = self.client.post(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest") self.assertEqual(response.status_code, 404)
def test_sql_explain_postgres_json_field(self): url = "/__debug__/sql_explain/" base_query = ( 'SELECT * FROM "tests_postgresjson" WHERE "tests_postgresjson"."field" @>' ) query = base_query + """ '{"foo": "bar"}'""" data = { "signed": SignedDataForm.sign({ "sql": query, "raw_sql": base_query + " %s", "params": '["{\\"foo\\": \\"bar\\"}"]', "alias": "default", "duration": "0", }) } response = self.client.post(url, data) self.assertEqual(response.status_code, 200) response = self.client.post(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest") self.assertEqual(response.status_code, 200) with self.settings(INTERNAL_IPS=[]): response = self.client.post(url, data) self.assertEqual(response.status_code, 404) response = self.client.post(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest") self.assertEqual(response.status_code, 404)
def history_refresh(request, verified_data): """Returns the refreshed list of table rows for the History Panel.""" form = HistoryStoreForm(verified_data) if form.is_valid(): requests = [] # Convert to list to handle mutations happenening in parallel for id, toolbar in list(DebugToolbar._store.items())[::-1]: requests.append({ "id": id, "content": render_to_string( "debug_toolbar/panels/history_tr.html", { "id": id, "store_context": { "toolbar": toolbar, "form": SignedDataForm(initial=HistoryStoreForm( initial={ "store_id": id }).initial), }, }, ), }) return JsonResponse({"requests": requests}) return HttpResponseBadRequest("Form errors")
def test_history_refresh(self): """Verify refresh history response has request variables.""" data = {"foo": "bar"} self.client.get("/json_view/", data, content_type="application/json") data = {"signed": SignedDataForm.sign({"store_id": "foo"})} response = self.client.get(reverse("djdt:history_refresh"), data=data) self.assertEqual(response.status_code, 200) data = response.json() self.assertEqual(len(data["requests"]), 1) for val in ["foo", "bar"]: self.assertIn(val, data["requests"][0]["content"])
def test_history_sidebar(self): """Validate the history sidebar view.""" self.client.get("/json_view/") store_id = list(DebugToolbar._store.keys())[0] data = {"signed": SignedDataForm.sign({"store_id": store_id})} response = self.client.get(reverse("djdt:history_sidebar"), data=data) self.assertEqual(response.status_code, 200) self.assertEqual( set(response.json().keys()), { "VersionsPanel", "TimerPanel", "SettingsPanel", "HeadersPanel", "RequestPanel", "SQLPanel", "StaticFilesPanel", "TemplatesPanel", "CachePanel", "SignalsPanel", "LoggingPanel", "ProfilingPanel", }, )
def generate_stats(self, request, response): colors = contrasting_color_generator() trace_colors = defaultdict(lambda: next(colors)) query_similar = defaultdict(lambda: defaultdict(int)) query_duplicates = defaultdict(lambda: defaultdict(int)) # The keys used to determine similar and duplicate queries. def similar_key(query): return query["raw_sql"] def duplicate_key(query): raw_params = ( () if query["raw_params"] is None else tuple(query["raw_params"]) ) # saferepr() avoids problems because of unhashable types # (e.g. lists) when used as dictionary keys. # https://github.com/jazzband/django-debug-toolbar/issues/1091 return (query["raw_sql"], saferepr(raw_params)) if self._queries: width_ratio_tally = 0 factor = int(256.0 / (len(self._databases) * 2.5)) for n, db in enumerate(self._databases.values()): rgb = [0, 0, 0] color = n % 3 rgb[color] = 256 - n // 3 * factor nn = color # XXX: pretty sure this is horrible after so many aliases while rgb[color] < factor: nc = min(256 - rgb[color], 256) rgb[color] += nc nn += 1 if nn > 2: nn = 0 rgb[nn] = nc db["rgb_color"] = rgb trans_ids = {} trans_id = None i = 0 for alias, query in self._queries: query_similar[alias][similar_key(query)] += 1 query_duplicates[alias][duplicate_key(query)] += 1 trans_id = query.get("trans_id") last_trans_id = trans_ids.get(alias) if trans_id != last_trans_id: if last_trans_id: self._queries[(i - 1)][1]["ends_trans"] = True trans_ids[alias] = trans_id if trans_id: query["starts_trans"] = True if trans_id: query["in_trans"] = True query["alias"] = alias if "iso_level" in query: query["iso_level"] = get_isolation_level_display( query["vendor"], query["iso_level"] ) if "trans_status" in query: query["trans_status"] = get_transaction_status_display( query["vendor"], query["trans_status"] ) query["form"] = SignedDataForm( auto_id=None, initial=SQLSelectForm(initial=copy(query)).initial ) if query["sql"]: query["sql"] = reformat_sql(query["sql"], with_toggle=True) query["rgb_color"] = self._databases[alias]["rgb_color"] try: query["width_ratio"] = (query["duration"] / self._sql_time) * 100 except ZeroDivisionError: query["width_ratio"] = 0 query["start_offset"] = width_ratio_tally query["end_offset"] = query["width_ratio"] + query["start_offset"] width_ratio_tally += query["width_ratio"] query["stacktrace"] = render_stacktrace(query["stacktrace"]) i += 1 query["trace_color"] = trace_colors[query["stacktrace"]] if trans_id: self._queries[(i - 1)][1]["ends_trans"] = True # Queries are similar / duplicates only if there's as least 2 of them. # Also, to hide queries, we need to give all the duplicate groups an id query_colors = contrasting_color_generator() query_similar_colors = { alias: { query: (similar_count, next(query_colors)) for query, similar_count in queries.items() if similar_count >= 2 } for alias, queries in query_similar.items() } query_duplicates_colors = { alias: { query: (duplicate_count, next(query_colors)) for query, duplicate_count in queries.items() if duplicate_count >= 2 } for alias, queries in query_duplicates.items() } for alias, query in self._queries: try: (query["similar_count"], query["similar_color"]) = query_similar_colors[ alias ][similar_key(query)] ( query["duplicate_count"], query["duplicate_color"], ) = query_duplicates_colors[alias][duplicate_key(query)] except KeyError: pass for alias, alias_info in self._databases.items(): try: alias_info["similar_count"] = sum( e[0] for e in query_similar_colors[alias].values() ) alias_info["duplicate_count"] = sum( e[0] for e in query_duplicates_colors[alias].values() ) except KeyError: pass self.record_stats( { "databases": sorted( self._databases.items(), key=lambda x: -x[1]["time_spent"] ), "queries": [q for a, q in self._queries], "sql_time": self._sql_time, } )
def test_prevents_tampering(self): data = { "signed": SIGNED_DATA.replace('"value": "foo"', '"value": "bar"') } form = SignedDataForm(data=data) self.assertFalse(form.is_valid())
def test_initial_set_signed(self): form = SignedDataForm(initial=DATA) self.assertEqual(form.initial["signed"], SIGNED_DATA)
def test_signed_data(self): data = {"signed": SignedDataForm.sign(DATA)} form = SignedDataForm(data=data) self.assertTrue(form.is_valid()) # Check the signature value self.assertEqual(data["signed"], SIGNED_DATA)
def generate_stats(self, request, response): colors = contrasting_color_generator() trace_colors = defaultdict(lambda: next(colors)) similar_query_groups = defaultdict(list) duplicate_query_groups = defaultdict(list) if self._queries: width_ratio_tally = 0 factor = int(256.0 / (len(self._databases) * 2.5)) for n, db in enumerate(self._databases.values()): rgb = [0, 0, 0] color = n % 3 rgb[color] = 256 - n // 3 * factor nn = color # XXX: pretty sure this is horrible after so many aliases while rgb[color] < factor: nc = min(256 - rgb[color], 256) rgb[color] += nc nn += 1 if nn > 2: nn = 0 rgb[nn] = nc db["rgb_color"] = rgb # the last query recorded for each DB alias last_by_alias = {} for query in self._queries: alias = query["alias"] similar_query_groups[(alias, _similar_query_key(query))].append(query) duplicate_query_groups[( alias, _duplicate_query_key(query))].append(query) trans_id = query.get("trans_id") prev_query = last_by_alias.get(alias, {}) prev_trans_id = prev_query.get("trans_id") # If two consecutive queries for a given DB alias have different # transaction ID values, a transaction started, finished, or both, so # annotate the queries as appropriate. if trans_id != prev_trans_id: if prev_trans_id is not None: prev_query["ends_trans"] = True if trans_id is not None: query["starts_trans"] = True if trans_id is not None: query["in_trans"] = True if "iso_level" in query: query["iso_level"] = get_isolation_level_display( query["vendor"], query["iso_level"]) if "trans_status" in query: query["trans_status"] = get_transaction_status_display( query["vendor"], query["trans_status"]) query["form"] = SignedDataForm( auto_id=None, initial=SQLSelectForm(initial=copy(query)).initial) if query["sql"]: query["sql"] = reformat_sql(query["sql"], with_toggle=True) query["rgb_color"] = self._databases[alias]["rgb_color"] try: query["width_ratio"] = (query["duration"] / self._sql_time) * 100 except ZeroDivisionError: query["width_ratio"] = 0 query["start_offset"] = width_ratio_tally query["end_offset"] = query["width_ratio"] + query[ "start_offset"] width_ratio_tally += query["width_ratio"] query["stacktrace"] = render_stacktrace(query["stacktrace"]) query["trace_color"] = trace_colors[query["stacktrace"]] last_by_alias[alias] = query # Close out any transactions that were in progress, since there is no # explicit way to know when a transaction finishes. for final_query in last_by_alias.values(): if final_query.get("trans_id") is not None: final_query["ends_trans"] = True group_colors = contrasting_color_generator() _process_query_groups(similar_query_groups, self._databases, group_colors, "similar") _process_query_groups(duplicate_query_groups, self._databases, group_colors, "duplicate") self.record_stats({ "databases": sorted(self._databases.items(), key=lambda x: -x[1]["time_spent"]), "queries": self._queries, "sql_time": self._sql_time, })