From a072dec3942afa8d6f5eca5bebd6cc4d03b3ff9e Mon Sep 17 00:00:00 2001 From: Nik Sauer Date: Tue, 28 Jan 2025 14:15:21 +0100 Subject: [PATCH] added initial station info test and not requiere sensor data in CreateStationDataAPIView endpoint --- app/api/views.py | 7 +- app/staticfiles/debug_toolbar/css/print.css | 3 + app/staticfiles/debug_toolbar/css/toolbar.css | 772 ++++++++++++++++++ app/staticfiles/debug_toolbar/js/history.js | 109 +++ app/staticfiles/debug_toolbar/js/redirect.js | 1 + app/staticfiles/debug_toolbar/js/timer.js | 88 ++ app/staticfiles/debug_toolbar/js/toolbar.js | 392 +++++++++ app/staticfiles/debug_toolbar/js/utils.js | 142 ++++ .../stag/api-device-initial-station-info.http | 23 + 9 files changed, 1535 insertions(+), 2 deletions(-) create mode 100644 app/staticfiles/debug_toolbar/css/print.css create mode 100644 app/staticfiles/debug_toolbar/css/toolbar.css create mode 100644 app/staticfiles/debug_toolbar/js/history.js create mode 100644 app/staticfiles/debug_toolbar/js/redirect.js create mode 100644 app/staticfiles/debug_toolbar/js/timer.js create mode 100644 app/staticfiles/debug_toolbar/js/toolbar.js create mode 100644 app/staticfiles/debug_toolbar/js/utils.js create mode 100644 tests/stag/api-device-initial-station-info.http diff --git a/app/api/views.py b/app/api/views.py index 9b2d676..f71c023 100644 --- a/app/api/views.py +++ b/app/api/views.py @@ -150,8 +150,8 @@ class CreateStationDataAPIView(APIView): def post(self, request, *args, **kwargs): # Parse the incoming JSON data try: - station_data = request.data.get('station') - sensors_data = request.data.get('sensors') + station_data = request.data.get('station', None) + sensors_data = request.data.get('sensors', None) if not station_data or not sensors_data: raise ValidationError("Both 'station' and 'sensors' are required.") @@ -165,6 +165,9 @@ def post(self, request, *args, **kwargs): # Record the time when the request was received time_received = datetime.datetime.now(datetime.timezone.utc) + if sensor_data: + return JsonResponse({"status": "success, but no sensor data found"}, status=200) + try: with transaction.atomic(): # Iterate through all sensors diff --git a/app/staticfiles/debug_toolbar/css/print.css b/app/staticfiles/debug_toolbar/css/print.css new file mode 100644 index 0000000..58d3084 --- /dev/null +++ b/app/staticfiles/debug_toolbar/css/print.css @@ -0,0 +1,3 @@ +#djDebug { + display: none !important; +} diff --git a/app/staticfiles/debug_toolbar/css/toolbar.css b/app/staticfiles/debug_toolbar/css/toolbar.css new file mode 100644 index 0000000..a8699a4 --- /dev/null +++ b/app/staticfiles/debug_toolbar/css/toolbar.css @@ -0,0 +1,772 @@ +/* Variable definitions */ +:root { + /* Font families are the same as in Django admin/css/base.css */ + --djdt-font-family-primary: "Segoe UI", system-ui, Roboto, "Helvetica Neue", + Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", + "Segoe UI Symbol", "Noto Color Emoji"; + --djdt-font-family-monospace: ui-monospace, Menlo, Monaco, "Cascadia Mono", + "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", + "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", + monospace, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; +} + +:root, +#djDebug[data-theme="light"] { + --djdt-font-color: black; + --djdt-background-color: white; + --djdt-panel-content-background-color: #eee; + --djdt-panel-content-table-background-color: var(--djdt-background-color); + --djdt-panel-title-background-color: #ffc; + --djdt-djdt-panel-content-table-strip-background-color: #f5f5f5; + --djdt--highlighted-background-color: lightgrey; + --djdt-toggle-template-background-color: #bbb; + + --djdt-sql-font-color: #333; + --djdt-pre-text-color: #555; + --djdt-path-and-locals: #777; + --djdt-stack-span-color: black; + --djdt-template-highlight-color: #333; + + --djdt-table-border-color: #ccc; + --djdt-button-border-color: var(--djdt-table-border-color); + --djdt-pre-border-color: var(--djdt-table-border-color); + --djdt-raw-border-color: var(--djdt-table-border-color); +} + +@media (prefers-color-scheme: dark) { + :root { + --djdt-font-color: #8393a7; + --djdt-background-color: #1e293bff; + --djdt-panel-content-background-color: #0f1729ff; + --djdt-panel-title-background-color: #242432; + --djdt-djdt-panel-content-table-strip-background-color: #324154ff; + --djdt--highlighted-background-color: #2c2a7dff; + --djdt-toggle-template-background-color: #282755; + + --djdt-sql-font-color: var(--djdt-font-color); + --djdt-pre-text-color: var(--djdt-font-color); + --djdt-path-and-locals: #65758cff; + --djdt-stack-span-color: #7c8fa4; + --djdt-template-highlight-color: var(--djdt-stack-span-color); + + --djdt-table-border-color: #324154ff; + --djdt-button-border-color: var(--djdt-table-border-color); + --djdt-pre-border-color: var(--djdt-table-border-color); + --djdt-raw-border-color: var(--djdt-table-border-color); + } +} + +#djDebug[data-theme="dark"] { + --djdt-font-color: #8393a7; + --djdt-background-color: #1e293bff; + --djdt-panel-content-background-color: #0f1729ff; + --djdt-panel-content-table-background-color: var(--djdt-background-color); + --djdt-panel-title-background-color: #242432; + --djdt-djdt-panel-content-table-strip-background-color: #324154ff; + --djdt--highlighted-background-color: #2c2a7dff; + --djdt-toggle-template-background-color: #282755; + + --djdt-sql-font-color: var(--djdt-font-color); + --djdt-pre-text-color: var(--djdt-font-color); + --djdt-path-and-locals: #65758cff; + --djdt-stack-span-color: #7c8fa4; + --djdt-template-highlight-color: var(--djdt-stack-span-color); + + --djdt-table-border-color: #324154ff; + --djdt-button-border-color: var(--djdt-table-border-color); + --djdt-pre-border-color: var(--djdt-table-border-color); + --djdt-raw-border-color: var(--djdt-table-border-color); +} + +/* Debug Toolbar CSS Reset, adapted from Eric Meyer's CSS Reset */ +#djDebug { + color: var(--djdt-font-color); + background: var(--djdt-background-color); +} +#djDebug, +#djDebug div, +#djDebug span, +#djDebug applet, +#djDebug object, +#djDebug iframe, +#djDebug h1, +#djDebug h2, +#djDebug h3, +#djDebug h4, +#djDebug h5, +#djDebug h6, +#djDebug p, +#djDebug blockquote, +#djDebug pre, +#djDebug a, +#djDebug abbr, +#djDebug acronym, +#djDebug address, +#djDebug big, +#djDebug cite, +#djDebug code, +#djDebug del, +#djDebug dfn, +#djDebug em, +#djDebug font, +#djDebug img, +#djDebug ins, +#djDebug kbd, +#djDebug q, +#djDebug s, +#djDebug samp, +#djDebug small, +#djDebug strike, +#djDebug strong, +#djDebug sub, +#djDebug sup, +#djDebug tt, +#djDebug var, +#djDebug b, +#djDebug u, +#djDebug i, +#djDebug center, +#djDebug dl, +#djDebug dt, +#djDebug dd, +#djDebug ol, +#djDebug ul, +#djDebug li, +#djDebug fieldset, +#djDebug form, +#djDebug label, +#djDebug legend, +#djDebug table, +#djDebug caption, +#djDebug tbody, +#djDebug tfoot, +#djDebug thead, +#djDebug tr, +#djDebug th, +#djDebug td, +#djDebug summary, +#djDebug button { + margin: 0; + padding: 0; + min-width: 0; + width: auto; + border: 0; + outline: 0; + font-size: 12px; + line-height: 1.5em; + color: var(--djdt-font-color); + vertical-align: baseline; + background-color: transparent; + font-family: var(--djdt-font-family-primary); + text-align: left; + text-shadow: none; + white-space: normal; + transition: none; +} + +#djDebug button { + background-color: #eee; + background-image: linear-gradient(to bottom, #eee, #cccccc); + border: 1px solid var(--djdt-button-border-color); + border-bottom: 1px solid #bbb; + border-radius: 3px; + color: #333; + line-height: 1; + padding: 0 8px; + text-align: center; + text-shadow: 0 1px 0 #eee; +} + +#djDebug button:hover { + background-color: #ddd; + background-image: linear-gradient(to bottom, #ddd, #bbb); + border-color: #bbb; + border-bottom-color: #999; + cursor: pointer; + text-shadow: 0 1px 0 #ddd; +} + +#djDebug button:active { + border: 1px solid #aaa; + border-bottom: 1px solid #888; + box-shadow: + inset 0 0 5px 2px #aaa, + 0 1px 0 0 #eee; +} + +#djDebug #djDebugToolbar { + background-color: #111; + width: 220px; + z-index: 100000000; + position: fixed; + top: 0; + bottom: 0; + right: 0; + opacity: 0.9; + overflow-y: auto; +} + +#djDebug #djDebugToolbar small { + color: #999; +} + +#djDebug #djDebugToolbar ul { + margin: 0; + padding: 0; + list-style: none; +} + +#djDebug #djDebugToolbar li { + border-bottom: 1px solid #222; + color: #fff; + display: block; + font-weight: bold; + float: none; + margin: 0; + padding: 0; + position: relative; + width: auto; +} + +#djDebug #djDebugToolbar input[type="checkbox"] { + float: right; + margin: 10px; +} + +#djDebug #djDebugToolbar li > a, +#djDebug #djDebugToolbar li > div.djdt-contentless { + font-weight: normal; + font-style: normal; + text-decoration: none; + display: block; + font-size: 16px; + padding: 7px 10px 8px 25px; + color: #fff; +} +#djDebug #djDebugToolbar li > div.djdt-disabled { + font-style: italic; + color: #999; +} + +#djDebug #djDebugToolbar li a:hover { + color: #111; + background-color: #ffc; +} + +#djDebug #djDebugToolbar li.djdt-active { + background: #333; +} + +#djDebug #djDebugToolbar li.djdt-active:before { + content: "▶"; + font-family: var(--djdt-font-family-primary); + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + color: #eee; + font-size: 150%; +} + +#djDebug #djDebugToolbar li.djdt-active a:hover { + color: #b36a60; + background-color: transparent; +} + +#djDebug #djDebugToolbar li small { + font-size: 12px; + color: #999; + font-style: normal; + text-decoration: none; +} + +#djDebug #djDebugToolbarHandle { + position: fixed; + transform: translateY(-100%) rotate(-90deg); + transform-origin: right bottom; + background-color: #fff; + border: 1px solid #111; + border-bottom: 0; + top: 0; + right: 0; + z-index: 100000000; + opacity: 0.75; +} + +#djDebug #djShowToolBarButton { + padding: 0 5px; + border: 4px solid #fff; + border-bottom-width: 0; + color: #fff; + font-size: 22px; + font-weight: bold; + background: #000; + opacity: 0.6; +} + +#djDebug #djShowToolBarButton:hover { + background-color: #111; + border-color: #ffe761; + cursor: move; + opacity: 1; +} + +#djDebug #djShowToolBarD { + color: #cf9; + font-size: 22px; +} + +#djDebug #djShowToolBarJ { + color: #cf9; + font-size: 16px; +} + +#djDebug pre, +#djDebug code { + display: block; + font-family: var(--djdt-font-family-monospace); + overflow: auto; +} + +#djDebug code { + font-size: 12px; + white-space: pre; +} + +#djDebug pre { + white-space: pre-wrap; + color: var(--djdt-pre-text-color); + border: 1px solid var(--djdt-pre-border-color); + border-collapse: collapse; + background-color: var(--djdt-background-color); + padding: 2px 3px; + margin-bottom: 3px; +} + +#djDebug .djdt-panelContent { + position: fixed; + margin: 0; + top: 0; + right: 220px; + bottom: 0; + left: 0px; + background-color: var(--djdt-panel-content-background-color); + color: #666; + z-index: 100000000; +} + +#djDebug .djdt-panelContent > div { + border-bottom: 1px solid #ddd; +} + +#djDebug .djDebugPanelTitle { + position: absolute; + background-color: var(--djdt-panel-title-background-color); + color: #666; + padding-left: 20px; + top: 0; + right: 0; + left: 0; + height: 50px; +} + +#djDebug .djDebugPanelTitle code { + display: inline; + font-size: inherit; +} + +#djDebug .djDebugPanelContent { + position: absolute; + top: 50px; + right: 0; + bottom: 0; + left: 0; + height: auto; + padding: 5px 0 0 20px; +} + +#djDebug .djDebugPanelContent .djdt-loader { + margin: 80px auto; + border: 6px solid white; + border-radius: 50%; + border-top: 6px solid #ffe761; + width: 38px; + height: 38px; + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +#djDebug .djDebugPanelContent .djdt-scroll { + height: 100%; + overflow: auto; + display: block; + padding: 0 10px 0 0; +} + +#djDebug h3 { + font-size: 24px; + font-weight: normal; + line-height: 50px; +} + +#djDebug h4 { + font-size: 20px; + font-weight: bold; + margin-top: 0.8em; +} + +#djDebug .djdt-panelContent table { + border: 1px solid var(--djdt-table-border-color); + border-collapse: collapse; + width: 100%; + background-color: var(--djdt-panel-content-table-background-color); + display: table; + margin-top: 0.8em; + overflow: auto; +} +#djDebug .djdt-panelContent tbody > tr:nth-child(odd):not(.djdt-highlighted) { + background-color: var(--djdt-panel-content-table-strip-background-color); +} +#djDebug .djdt-panelContent tbody td, +#djDebug .djdt-panelContent tbody th { + vertical-align: top; + padding: 2px 3px; +} +#djDebug .djdt-panelContent tbody td.djdt-time { + text-align: center; +} + +#djDebug .djdt-panelContent thead th { + padding: 1px 6px 1px 3px; + text-align: left; + font-weight: bold; + font-size: 14px; + white-space: nowrap; +} +#djDebug .djdt-panelContent tbody th { + width: 12em; + text-align: right; + color: #666; + padding-right: 0.5em; +} + +#djDebug .djTemplateContext { + background-color: var(--djdt-background-color); +} + +#djDebug .djdt-panelContent .djDebugClose { + position: absolute; + top: 4px; + right: 15px; + line-height: 16px; + border: 6px solid #ddd; + border-radius: 50%; + background: #fff; + color: #ddd; + font-weight: 900; + font-size: 20px; + height: 36px; + width: 36px; + padding: 0 0 5px; + box-sizing: border-box; + display: grid; + place-items: center; +} + +#djDebug .djdt-panelContent .djDebugClose:hover { + background: #c0695d; +} + +#djDebug .djdt-panelContent dt, +#djDebug .djdt-panelContent dd { + display: block; +} + +#djDebug .djdt-panelContent dt { + margin-top: 0.75em; +} + +#djDebug .djdt-panelContent dd { + margin-left: 10px; +} + +#djDebug a.toggleTemplate { + padding: 4px; + background-color: var(--djdt-toggle-template-background-color); + border-radius: 3px; +} + +#djDebug a.toggleTemplate:hover { + padding: 4px; + background-color: #444; + color: #ffe761; + border-radius: 3px; +} + +#djDebug .djDebugCollapsed { + color: var(--djdt-sql-font-color); +} + +#djDebug .djDebugUncollapsed { + color: var(--djdt-sql-font-color); +} + +#djDebug .djUnselected { + display: none; +} + +#djDebug tr.djSelected { + display: table-row; +} + +#djDebug .djDebugSql { + overflow-wrap: anywhere; +} + +#djDebug .djSQLDetailsDiv tbody th { + text-align: left; +} + +#djDebug span.djDebugLineChart { + background-color: #777; + height: 3px; + position: absolute; + bottom: 0; + top: 0; + left: 0; + display: block; + z-index: 1000000001; +} +#djDebug span.djDebugLineChartWarning { + background-color: #900; +} + +#djDebug .highlight { + color: var(--djdt-font-color); +} +#djDebug .highlight .err { + color: var(--djdt-font-color); +} /* Error */ + +/* +Styles for pygments HTMLFormatter + +- This should match debug_toolbar/panels/templates/views.py::template_source +- Each line needs to be prefixed with #djDebug .highlight as well. +- The .w definition needs to include: + white-space: pre-wrap + +To regenerate: + + from pygments.formatters import HtmlFormatter + print(HtmlFormatter(wrapcode=True).get_style_defs()) + */ +#djDebug .highlight pre { line-height: 125%; } +#djDebug .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +#djDebug .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +#djDebug .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +#djDebug .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +#djDebug .highlight .hll { background-color: #ffffcc } +#djDebug .highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ +#djDebug .highlight .err { border: 1px solid #FF0000 } /* Error */ +#djDebug .highlight .k { color: #008000; font-weight: bold } /* Keyword */ +#djDebug .highlight .o { color: #666666 } /* Operator */ +#djDebug .highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ +#djDebug .highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ +#djDebug .highlight .cp { color: #9C6500 } /* Comment.Preproc */ +#djDebug .highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ +#djDebug .highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ +#djDebug .highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ +#djDebug .highlight .gd { color: #A00000 } /* Generic.Deleted */ +#djDebug .highlight .ge { font-style: italic } /* Generic.Emph */ +#djDebug .highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +#djDebug .highlight .gr { color: #E40000 } /* Generic.Error */ +#djDebug .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +#djDebug .highlight .gi { color: #008400 } /* Generic.Inserted */ +#djDebug .highlight .go { color: #717171 } /* Generic.Output */ +#djDebug .highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +#djDebug .highlight .gs { font-weight: bold } /* Generic.Strong */ +#djDebug .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +#djDebug .highlight .gt { color: #0044DD } /* Generic.Traceback */ +#djDebug .highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +#djDebug .highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +#djDebug .highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +#djDebug .highlight .kp { color: #008000 } /* Keyword.Pseudo */ +#djDebug .highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +#djDebug .highlight .kt { color: #B00040 } /* Keyword.Type */ +#djDebug .highlight .m { color: #666666 } /* Literal.Number */ +#djDebug .highlight .s { color: #BA2121 } /* Literal.String */ +#djDebug .highlight .na { color: #687822 } /* Name.Attribute */ +#djDebug .highlight .nb { color: #008000 } /* Name.Builtin */ +#djDebug .highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +#djDebug .highlight .no { color: #880000 } /* Name.Constant */ +#djDebug .highlight .nd { color: #AA22FF } /* Name.Decorator */ +#djDebug .highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ +#djDebug .highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ +#djDebug .highlight .nf { color: #0000FF } /* Name.Function */ +#djDebug .highlight .nl { color: #767600 } /* Name.Label */ +#djDebug .highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +#djDebug .highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ +#djDebug .highlight .nv { color: #19177C } /* Name.Variable */ +#djDebug .highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +#djDebug .highlight .w { color: #bbbbbb; white-space: pre-wrap } /* Text.Whitespace */ +#djDebug .highlight .mb { color: #666666 } /* Literal.Number.Bin */ +#djDebug .highlight .mf { color: #666666 } /* Literal.Number.Float */ +#djDebug .highlight .mh { color: #666666 } /* Literal.Number.Hex */ +#djDebug .highlight .mi { color: #666666 } /* Literal.Number.Integer */ +#djDebug .highlight .mo { color: #666666 } /* Literal.Number.Oct */ +#djDebug .highlight .sa { color: #BA2121 } /* Literal.String.Affix */ +#djDebug .highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +#djDebug .highlight .sc { color: #BA2121 } /* Literal.String.Char */ +#djDebug .highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ +#djDebug .highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +#djDebug .highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +#djDebug .highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +#djDebug .highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +#djDebug .highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +#djDebug .highlight .sx { color: #008000 } /* Literal.String.Other */ +#djDebug .highlight .sr { color: #A45A77 } /* Literal.String.Regex */ +#djDebug .highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +#djDebug .highlight .ss { color: #19177C } /* Literal.String.Symbol */ +#djDebug .highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +#djDebug .highlight .fm { color: #0000FF } /* Name.Function.Magic */ +#djDebug .highlight .vc { color: #19177C } /* Name.Variable.Class */ +#djDebug .highlight .vg { color: #19177C } /* Name.Variable.Global */ +#djDebug .highlight .vi { color: #19177C } /* Name.Variable.Instance */ +#djDebug .highlight .vm { color: #19177C } /* Name.Variable.Magic */ +#djDebug .highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ + +#djDebug svg.djDebugLineChart { + width: 100%; + height: 1.5em; +} + +#djDebug svg.djDebugLineChartWarning rect { + fill: #900; +} + +#djDebug svg.djDebugLineChartInTransaction rect { + fill: #d3ff82; +} +#djDebug svg.djDebugLineChart line { + stroke: #94b24d; +} + +#djDebug .djDebugRowWarning .djdt-time { + color: red; +} +#djDebug .djdt-panelContent table .djdt-toggle { + width: 14px; + padding-top: 3px; +} +#djDebug .djdt-panelContent table .djdt-actions { + min-width: 70px; + white-space: nowrap; +} +#djDebug .djdt-color:after { + content: "\00a0"; +} +#djDebug .djToggleSwitch { + box-sizing: content-box; + padding: 0; + border: 1px solid #999; + border-radius: 0; + width: 12px; + color: #777; + background: linear-gradient(to bottom, #fff, #dcdcdc); +} +#djDebug .djNoToggleSwitch { + height: 14px; + width: 14px; + display: inline-block; +} + +#djDebug .djSQLDetailsDiv { + margin-top: 0.8em; +} + +#djDebug .djdt-stack span { + color: var(--djdt-stack-span-color); + font-weight: bold; +} +#djDebug .djdt-stack span.djdt-path, +#djDebug .djdt-stack pre.djdt-locals, +#djDebug .djdt-stack pre.djdt-locals span { + color: var(--djdt-path-and-locals); + font-weight: normal; +} +#djDebug .djdt-stack span.djdt-code { + font-weight: normal; +} +#djDebug .djdt-stack pre.djdt-locals { + margin: 0 27px 27px 27px; +} +#djDebug .djdt-raw { + background-color: #fff; + border: 1px solid var(--djdt-raw-border-color); + margin-top: 0.8em; + padding: 5px; + white-space: pre-wrap; +} + +#djDebug .djdt-width-20 { + width: 20%; +} +#djDebug .djdt-width-30 { + width: 30%; +} +#djDebug .djdt-width-60 { + width: 60%; +} +#djDebug .djdt-max-height-100 { + max-height: 100%; +} +#djDebug .djdt-highlighted { + background-color: var(--djdt--highlighted-background-color); +} +#djDebug tr.djdt-highlighted.djdt-profile-row { + background-color: #ffc; +} +#djDebug tr.djdt-highlighted.djdt-profile-row:nth-child(2n + 1) { + background-color: #dd9; +} +@keyframes djdt-flash-new { + from { + background-color: green; + } + to { + background-color: inherit; + } +} +#djDebug .flash-new { + animation: djdt-flash-new 1s; +} + +.djdt-hidden { + display: none; +} + +#djDebug #djDebugToolbar a#djToggleThemeButton { + display: flex; + align-items: center; + cursor: pointer; +} +#djToggleThemeButton > svg { + margin-left: auto; +} +#djDebug[data-theme="light"] #djToggleThemeButton svg.theme-light, +#djDebug[data-theme="dark"] #djToggleThemeButton svg.theme-dark, +#djDebug[data-theme="auto"] #djToggleThemeButton svg.theme-auto { + display: block; + height: 1rem; + width: 1rem; +} diff --git a/app/staticfiles/debug_toolbar/js/history.js b/app/staticfiles/debug_toolbar/js/history.js new file mode 100644 index 0000000..314ddb3 --- /dev/null +++ b/app/staticfiles/debug_toolbar/js/history.js @@ -0,0 +1,109 @@ +import { $$, ajaxForm, replaceToolbarState } from "./utils.js"; + +const djDebug = document.getElementById("djDebug"); + +function difference(setA, setB) { + const _difference = new Set(setA); + for (const elem of setB) { + _difference.delete(elem); + } + return _difference; +} + +/** + * Create an array of dataset properties from a NodeList. + */ +function pluckData(nodes, key) { + const data = []; + nodes.forEach(function (obj) { + data.push(obj.dataset[key]); + }); + return data; +} + +function refreshHistory() { + const formTarget = djDebug.querySelector(".refreshHistory"); + const container = document.getElementById("djdtHistoryRequests"); + const oldIds = new Set( + pluckData(container.querySelectorAll("tr[data-store-id]"), "storeId") + ); + + ajaxForm(formTarget) + .then(function (data) { + // Remove existing rows first then re-populate with new data + container + .querySelectorAll("tr[data-store-id]") + .forEach(function (node) { + node.remove(); + }); + data.requests.forEach(function (request) { + container.innerHTML = request.content + container.innerHTML; + }); + }) + .then(function () { + const allIds = new Set( + pluckData( + container.querySelectorAll("tr[data-store-id]"), + "storeId" + ) + ); + const newIds = difference(allIds, oldIds); + const lastRequestId = newIds.values().next().value; + return { + allIds, + newIds, + lastRequestId, + }; + }) + .then(function (refreshInfo) { + refreshInfo.newIds.forEach(function (newId) { + const row = container.querySelector( + `tr[data-store-id="${newId}"]` + ); + row.classList.add("flash-new"); + }); + setTimeout(() => { + container + .querySelectorAll("tr[data-store-id]") + .forEach((row) => { + row.classList.remove("flash-new"); + }); + }, 2000); + }); +} + +function switchHistory(newStoreId) { + const formTarget = djDebug.querySelector( + ".switchHistory[data-store-id='" + newStoreId + "']" + ); + const tbody = formTarget.closest("tbody"); + + const highlighted = tbody.querySelector(".djdt-highlighted"); + if (highlighted) { + highlighted.classList.remove("djdt-highlighted"); + } + formTarget.closest("tr").classList.add("djdt-highlighted"); + + ajaxForm(formTarget).then(function (data) { + if (Object.keys(data).length === 0) { + const container = document.getElementById("djdtHistoryRequests"); + container.querySelector( + 'button[data-store-id="' + newStoreId + '"]' + ).innerHTML = "Switch [EXPIRED]"; + } + replaceToolbarState(newStoreId, data); + }); +} + +$$.on(djDebug, "click", ".switchHistory", function (event) { + event.preventDefault(); + switchHistory(this.dataset.storeId); +}); + +$$.on(djDebug, "click", ".refreshHistory", function (event) { + event.preventDefault(); + refreshHistory(); +}); +// We don't refresh the whole toolbar each fetch or ajax request, +// so we need to refresh the history when we open the panel +$$.onPanelRender(djDebug, "HistoryPanel", refreshHistory); diff --git a/app/staticfiles/debug_toolbar/js/redirect.js b/app/staticfiles/debug_toolbar/js/redirect.js new file mode 100644 index 0000000..6976de4 --- /dev/null +++ b/app/staticfiles/debug_toolbar/js/redirect.js @@ -0,0 +1 @@ +document.getElementById("redirect_to").focus(); diff --git a/app/staticfiles/debug_toolbar/js/timer.js b/app/staticfiles/debug_toolbar/js/timer.js new file mode 100644 index 0000000..a88ab0d --- /dev/null +++ b/app/staticfiles/debug_toolbar/js/timer.js @@ -0,0 +1,88 @@ +import { $$ } from "./utils.js"; + +function insertBrowserTiming() { + const timingOffset = performance.timing.navigationStart, + timingEnd = performance.timing.loadEventEnd, + totalTime = timingEnd - timingOffset; + function getLeft(stat) { + if (totalTime !== 0) { + return ( + ((performance.timing[stat] - timingOffset) / totalTime) * 100.0 + ); + } else { + return 0; + } + } + function getCSSWidth(stat, endStat) { + let width = 0; + if (totalTime !== 0) { + width = + ((performance.timing[endStat] - performance.timing[stat]) / + totalTime) * + 100.0; + } + const denominator = 100.0 - getLeft(stat); + if (denominator !== 0) { + // Calculate relative percent (same as sql panel logic) + width = (100.0 * width) / denominator; + } else { + width = 0; + } + return width < 1 ? "2px" : width + "%"; + } + function addRow(tbody, stat, endStat) { + const row = document.createElement("tr"); + if (endStat) { + // Render a start through end bar + row.innerHTML = + "" + + stat.replace("Start", "") + + "" + + '' + + "" + + (performance.timing[stat] - timingOffset) + + " (+" + + (performance.timing[endStat] - performance.timing[stat]) + + ")"; + row.querySelector("rect").setAttribute( + "width", + getCSSWidth(stat, endStat) + ); + } else { + // Render a point in time + row.innerHTML = + "" + + stat + + "" + + '' + + "" + + (performance.timing[stat] - timingOffset) + + ""; + row.querySelector("rect").setAttribute("width", 2); + } + row.querySelector("rect").setAttribute("x", getLeft(stat)); + tbody.appendChild(row); + } + + const browserTiming = document.getElementById("djDebugBrowserTiming"); + // Determine if the browser timing section has already been rendered. + if (browserTiming.classList.contains("djdt-hidden")) { + const tbody = document.getElementById("djDebugBrowserTimingTableBody"); + // This is a reasonably complete and ordered set of timing periods (2 params) and events (1 param) + addRow(tbody, "domainLookupStart", "domainLookupEnd"); + addRow(tbody, "connectStart", "connectEnd"); + addRow(tbody, "requestStart", "responseEnd"); // There is no requestEnd + addRow(tbody, "responseStart", "responseEnd"); + addRow(tbody, "domLoading", "domComplete"); // Spans the events below + addRow(tbody, "domInteractive"); + addRow(tbody, "domContentLoadedEventStart", "domContentLoadedEventEnd"); + addRow(tbody, "loadEventStart", "loadEventEnd"); + browserTiming.classList.remove("djdt-hidden"); + } +} + +const djDebug = document.getElementById("djDebug"); +// Insert the browser timing now since it's possible for this +// script to miss the initial panel load event. +insertBrowserTiming(); +$$.onPanelRender(djDebug, "TimerPanel", insertBrowserTiming); diff --git a/app/staticfiles/debug_toolbar/js/toolbar.js b/app/staticfiles/debug_toolbar/js/toolbar.js new file mode 100644 index 0000000..067b5a3 --- /dev/null +++ b/app/staticfiles/debug_toolbar/js/toolbar.js @@ -0,0 +1,392 @@ +import { $$, ajax, debounce, replaceToolbarState } from "./utils.js"; + +function onKeyDown(event) { + if (event.keyCode === 27) { + djdt.hideOneLevel(); + } +} + +function getDebugElement() { + // Fetch the debug element from the DOM. + // This is used to avoid writing the element's id + // everywhere the element is being selected. A fixed reference + // to the element should be avoided because the entire DOM could + // be reloaded such as via HTMX boosting. + return document.getElementById("djDebug"); +} + +const djdt = { + handleDragged: false, + needUpdateOnFetch: false, + init() { + const djDebug = getDebugElement(); + djdt.needUpdateOnFetch = djDebug.dataset.updateOnFetch === "True"; + $$.on(djDebug, "click", "#djDebugPanelList li a", function (event) { + event.preventDefault(); + if (!this.className) { + return; + } + const panelId = this.className; + const current = document.getElementById(panelId); + if ($$.visible(current)) { + djdt.hidePanels(); + } else { + djdt.hidePanels(); + + $$.show(current); + this.parentElement.classList.add("djdt-active"); + + const inner = current.querySelector( + ".djDebugPanelContent .djdt-scroll" + ), + storeId = djDebug.dataset.storeId; + if (storeId && inner.children.length === 0) { + const url = new URL( + djDebug.dataset.renderPanelUrl, + window.location + ); + url.searchParams.append("store_id", storeId); + url.searchParams.append("panel_id", panelId); + ajax(url).then(function (data) { + inner.previousElementSibling.remove(); // Remove AJAX loader + inner.innerHTML = data.content; + $$.executeScripts(data.scripts); + $$.applyStyles(inner); + djDebug.dispatchEvent( + new CustomEvent("djdt.panel.render", { + detail: { panelId: panelId }, + }) + ); + }); + } else { + djDebug.dispatchEvent( + new CustomEvent("djdt.panel.render", { + detail: { panelId: panelId }, + }) + ); + } + } + }); + $$.on(djDebug, "click", ".djDebugClose", function () { + djdt.hideOneLevel(); + }); + $$.on( + djDebug, + "click", + ".djDebugPanelButton input[type=checkbox]", + function () { + djdt.cookie.set( + this.dataset.cookie, + this.checked ? "on" : "off", + { + path: "/", + expires: 10, + } + ); + } + ); + + // Used by the SQL and template panels + $$.on(djDebug, "click", ".remoteCall", function (event) { + event.preventDefault(); + + let url; + const ajaxData = {}; + + if (this.tagName === "BUTTON") { + const form = this.closest("form"); + url = this.formAction; + ajaxData.method = form.method.toUpperCase(); + ajaxData.body = new FormData(form); + } else if (this.tagName === "A") { + url = this.href; + } + + ajax(url, ajaxData).then(function (data) { + const win = document.getElementById("djDebugWindow"); + win.innerHTML = data.content; + $$.show(win); + }); + }); + + // Used by the cache, profiling and SQL panels + $$.on(djDebug, "click", ".djToggleSwitch", function () { + const id = this.dataset.toggleId; + const toggleOpen = "+"; + const toggleClose = "-"; + const openMe = this.textContent === toggleOpen; + const name = this.dataset.toggleName; + const container = document.getElementById(name + "_" + id); + container + .querySelectorAll(".djDebugCollapsed") + .forEach(function (e) { + $$.toggle(e, openMe); + }); + container + .querySelectorAll(".djDebugUncollapsed") + .forEach(function (e) { + $$.toggle(e, !openMe); + }); + const self = this; + this.closest(".djDebugPanelContent") + .querySelectorAll(".djToggleDetails_" + id) + .forEach(function (e) { + if (openMe) { + e.classList.add("djSelected"); + e.classList.remove("djUnselected"); + self.textContent = toggleClose; + } else { + e.classList.remove("djSelected"); + e.classList.add("djUnselected"); + self.textContent = toggleOpen; + } + const switch_ = e.querySelector(".djToggleSwitch"); + if (switch_) { + switch_.textContent = self.textContent; + } + }); + }); + + $$.on(djDebug, "click", "#djHideToolBarButton", function (event) { + event.preventDefault(); + djdt.hideToolbar(); + }); + + $$.on(djDebug, "click", "#djShowToolBarButton", function () { + if (!djdt.handleDragged) { + djdt.showToolbar(); + } + }); + let startPageY, baseY; + const handle = document.getElementById("djDebugToolbarHandle"); + function onHandleMove(event) { + // Chrome can send spurious mousemove events, so don't do anything unless the + // cursor really moved. Otherwise, it will be impossible to expand the toolbar + // due to djdt.handleDragged being set to true. + if (djdt.handleDragged || event.pageY !== startPageY) { + let top = baseY + event.pageY; + + if (top < 0) { + top = 0; + } else if (top + handle.offsetHeight > window.innerHeight) { + top = window.innerHeight - handle.offsetHeight; + } + + handle.style.top = top + "px"; + djdt.handleDragged = true; + } + } + $$.on(djDebug, "mousedown", "#djShowToolBarButton", function (event) { + event.preventDefault(); + startPageY = event.pageY; + baseY = handle.offsetTop - startPageY; + document.addEventListener("mousemove", onHandleMove); + + document.addEventListener( + "mouseup", + function (event) { + document.removeEventListener("mousemove", onHandleMove); + if (djdt.handleDragged) { + event.preventDefault(); + localStorage.setItem("djdt.top", handle.offsetTop); + requestAnimationFrame(function () { + djdt.handleDragged = false; + }); + djdt.ensureHandleVisibility(); + } + }, + { once: true } + ); + }); + + // Make sure the debug element is rendered at least once. + // showToolbar will continue to show it in the future if the + // entire DOM is reloaded. + $$.show(djDebug); + const show = + localStorage.getItem("djdt.show") || djDebug.dataset.defaultShow; + if (show === "true") { + djdt.showToolbar(); + } else { + djdt.hideToolbar(); + } + if (djDebug.dataset.sidebarUrl !== undefined) { + djdt.updateOnAjax(); + } + + // Updates the theme using user settings + const userTheme = localStorage.getItem("djdt.user-theme"); + if (userTheme !== null) { + djDebug.setAttribute("data-theme", userTheme); + } + // Adds the listener to the Theme Toggle Button + $$.on(djDebug, "click", "#djToggleThemeButton", function () { + switch (djDebug.getAttribute("data-theme")) { + case "auto": + djDebug.setAttribute("data-theme", "light"); + localStorage.setItem("djdt.user-theme", "light"); + break; + case "light": + djDebug.setAttribute("data-theme", "dark"); + localStorage.setItem("djdt.user-theme", "dark"); + break; + default: /* dark is the default */ + djDebug.setAttribute("data-theme", "auto"); + localStorage.setItem("djdt.user-theme", "auto"); + break; + } + }); + }, + hidePanels() { + const djDebug = getDebugElement(); + $$.hide(document.getElementById("djDebugWindow")); + djDebug.querySelectorAll(".djdt-panelContent").forEach(function (e) { + $$.hide(e); + }); + document.querySelectorAll("#djDebugToolbar li").forEach(function (e) { + e.classList.remove("djdt-active"); + }); + }, + ensureHandleVisibility() { + const handle = document.getElementById("djDebugToolbarHandle"); + // set handle position + const handleTop = Math.min( + localStorage.getItem("djdt.top") || 265, + window.innerHeight - handle.offsetWidth + ); + handle.style.top = handleTop + "px"; + }, + hideToolbar() { + djdt.hidePanels(); + + $$.hide(document.getElementById("djDebugToolbar")); + + const handle = document.getElementById("djDebugToolbarHandle"); + $$.show(handle); + djdt.ensureHandleVisibility(); + window.addEventListener("resize", djdt.ensureHandleVisibility); + document.removeEventListener("keydown", onKeyDown); + + localStorage.setItem("djdt.show", "false"); + }, + hideOneLevel() { + const win = document.getElementById("djDebugWindow"); + if ($$.visible(win)) { + $$.hide(win); + } else { + const toolbar = document.getElementById("djDebugToolbar"); + if (toolbar.querySelector("li.djdt-active")) { + djdt.hidePanels(); + } else { + djdt.hideToolbar(); + } + } + }, + showToolbar() { + document.addEventListener("keydown", onKeyDown); + $$.show(document.getElementById("djDebug")); + $$.hide(document.getElementById("djDebugToolbarHandle")); + $$.show(document.getElementById("djDebugToolbar")); + localStorage.setItem("djdt.show", "true"); + window.removeEventListener("resize", djdt.ensureHandleVisibility); + }, + updateOnAjax() { + const sidebarUrl = + document.getElementById("djDebug").dataset.sidebarUrl; + const slowjax = debounce(ajax, 200); + + function handleAjaxResponse(storeId) { + storeId = encodeURIComponent(storeId); + const dest = `${sidebarUrl}?store_id=${storeId}`; + slowjax(dest).then(function (data) { + if (djdt.needUpdateOnFetch) { + replaceToolbarState(storeId, data); + } + }); + } + + // Patch XHR / traditional AJAX requests + const origOpen = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = function () { + this.addEventListener("load", function () { + // Chromium emits a "Refused to get unsafe header" uncatchable warning + // when the header can't be fetched. While it doesn't impede execution + // it's worrisome to developers. + if ( + this.getAllResponseHeaders().indexOf("djdt-store-id") >= 0 + ) { + handleAjaxResponse(this.getResponseHeader("djdt-store-id")); + } + }); + origOpen.apply(this, arguments); + }; + + const origFetch = window.fetch; + window.fetch = function () { + const promise = origFetch.apply(this, arguments); + promise.then(function (response) { + if (response.headers.get("djdt-store-id") !== null) { + handleAjaxResponse(response.headers.get("djdt-store-id")); + } + // Don't resolve the response via .json(). Instead + // continue to return it to allow the caller to consume as needed. + return response; + }); + return promise; + }; + }, + cookie: { + get(key) { + if (!document.cookie.includes(key)) { + return null; + } + + const cookieArray = document.cookie.split("; "), + cookies = {}; + + cookieArray.forEach(function (e) { + const parts = e.split("="); + cookies[parts[0]] = parts[1]; + }); + + return cookies[key]; + }, + set(key, value, options) { + options = options || {}; + + if (typeof options.expires === "number") { + const days = options.expires, + t = (options.expires = new Date()); + t.setDate(t.getDate() + days); + } + + document.cookie = [ + encodeURIComponent(key) + "=" + String(value), + options.expires + ? "; expires=" + options.expires.toUTCString() + : "", + options.path ? "; path=" + options.path : "", + options.domain ? "; domain=" + options.domain : "", + options.secure ? "; secure" : "", + "samesite" in options + ? "; samesite=" + options.samesite + : "; samesite=lax", + ].join(""); + + return value; + }, + }, +}; +window.djdt = { + show_toolbar: djdt.showToolbar, + hide_toolbar: djdt.hideToolbar, + init: djdt.init, + close: djdt.hideOneLevel, + cookie: djdt.cookie, +}; + +if (document.readyState !== "loading") { + djdt.init(); +} else { + document.addEventListener("DOMContentLoaded", djdt.init); +} diff --git a/app/staticfiles/debug_toolbar/js/utils.js b/app/staticfiles/debug_toolbar/js/utils.js new file mode 100644 index 0000000..c37525f --- /dev/null +++ b/app/staticfiles/debug_toolbar/js/utils.js @@ -0,0 +1,142 @@ +const $$ = { + on(root, eventName, selector, fn) { + root.removeEventListener(eventName, fn); + root.addEventListener(eventName, function (event) { + const target = event.target.closest(selector); + if (root.contains(target)) { + fn.call(target, event); + } + }); + }, + onPanelRender(root, panelId, fn) { + /* + This is a helper function to attach a handler for a `djdt.panel.render` + event of a specific panel. + + root: The container element that the listener should be attached to. + panelId: The Id of the panel. + fn: A function to execute when the event is triggered. + */ + root.addEventListener("djdt.panel.render", function (event) { + if (event.detail.panelId === panelId) { + fn.call(event); + } + }); + }, + show(element) { + element.classList.remove("djdt-hidden"); + }, + hide(element) { + element.classList.add("djdt-hidden"); + }, + toggle(element, value) { + if (value) { + $$.show(element); + } else { + $$.hide(element); + } + }, + visible(element) { + return !element.classList.contains("djdt-hidden"); + }, + executeScripts(scripts) { + scripts.forEach(function (script) { + const el = document.createElement("script"); + el.type = "module"; + el.src = script; + el.async = true; + document.head.appendChild(el); + }); + }, + applyStyles(container) { + /* + * Given a container element, apply styles set via data-djdt-styles attribute. + * The format is data-djdt-styles="styleName1:value;styleName2:value2" + * The style names should use the CSSStyleDeclaration camel cased names. + */ + container + .querySelectorAll("[data-djdt-styles]") + .forEach(function (element) { + const styles = element.dataset.djdtStyles || ""; + styles.split(";").forEach(function (styleText) { + const styleKeyPair = styleText.split(":"); + if (styleKeyPair.length === 2) { + const name = styleKeyPair[0].trim(); + const value = styleKeyPair[1].trim(); + element.style[name] = value; + } + }); + }); + }, +}; + +function ajax(url, init) { + init = Object.assign({ credentials: "same-origin" }, init); + return fetch(url, init) + .then(function (response) { + if (response.ok) { + return response.json().catch(function(error){ + return Promise.reject( + new Error("The response is a invalid Json object : " + error) + ); + }); + } + return Promise.reject( + new Error(response.status + ": " + response.statusText) + ); + }) + .catch(function (error) { + const win = document.getElementById("djDebugWindow"); + win.innerHTML = + '

' + + error.message + + "

"; + $$.show(win); + throw error; + }); +} + +function ajaxForm(element) { + const form = element.closest("form"); + const url = new URL(form.action); + const formData = new FormData(form); + for (const [name, value] of formData.entries()) { + url.searchParams.append(name, value); + } + const ajaxData = { + method: form.method.toUpperCase(), + }; + return ajax(url, ajaxData); +} + +function replaceToolbarState(newStoreId, data) { + const djDebug = document.getElementById("djDebug"); + djDebug.setAttribute("data-store-id", newStoreId); + // Check if response is empty, it could be due to an expired storeId. + Object.keys(data).forEach(function (panelId) { + const panel = document.getElementById(panelId); + if (panel) { + panel.outerHTML = data[panelId].content; + document.getElementById("djdt-" + panelId).outerHTML = + data[panelId].button; + } + }); +} + +function debounce(func, delay) { + let timer = null; + let resolves = []; + + return function (...args) { + clearTimeout(timer); + timer = setTimeout(() => { + const result = func(...args); + resolves.forEach((r) => r(result)); + resolves = []; + }, delay); + + return new Promise((r) => resolves.push(r)); + }; +} + +export { $$, ajax, ajaxForm, replaceToolbarState, debounce }; diff --git a/tests/stag/api-device-initial-station-info.http b/tests/stag/api-device-initial-station-info.http new file mode 100644 index 0000000..76fc1ed --- /dev/null +++ b/tests/stag/api-device-initial-station-info.http @@ -0,0 +1,23 @@ +# API test request for the endpoint /api/device/status +# + +POST https://staging.datahub.luftdaten.at/api/v1/devices/data/ HTTP/1.1 +content-type: application/json + +{ + "station": { + "time": "2025-01-07T11:23:23.439Z", + "device": "string", + "firmware": "string", + "model": 0, + "apikey": "initialStationInfoTest", + + "sensor_list": [ + { + "model_id": 1, + "dimension_list": [1, 2, 3] + } + ] + }, + "sensors": {} +} \ No newline at end of file