diff --git a/backend/htmlcov/class_index.html b/backend/htmlcov/class_index.html new file mode 100644 index 0000000..520fba1 --- /dev/null +++ b/backend/htmlcov/class_index.html @@ -0,0 +1,821 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 84% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fileclass statementsmissingexcluded coverage
app / __init__.py(no class) 2900 100%
app / config.pyConfig 000 100%
app / config.pyDevelopmentConfig 000 100%
app / config.pyTestingConfig 000 100%
app / config.pyProductionConfig 000 100%
app / config.py(no class) 1600 100%
app / dto / __init__.py(no class) 000 100%
app / dto / addiction_dto.pyAddictionItemDTO 200 100%
app / dto / addiction_dto.pyAddictionListResponseDTO 310 67%
app / dto / addiction_dto.py(no class) 1600 100%
app / dto / dashboard_dto.pyDayStatusDTO 100 100%
app / dto / dashboard_dto.pyDashboardResponseDTO 100 100%
app / dto / dashboard_dto.py(no class) 1500 100%
app / dto / days_dto.pyUpdateDayStatusRequest 110 0%
app / dto / days_dto.pyDayEntryResponse 100 100%
app / dto / days_dto.py(no class) 1100 100%
app / dto / progress_dto.pyDayEntryDTO 100 100%
app / dto / progress_dto.pyProgressResponseDTO 100 100%
app / dto / progress_dto.py(no class) 1500 100%
app / dto / user_addiction_dto.pyAddUserAddictionRequestDTO 110 0%
app / dto / user_addiction_dto.pyUserAddictionResponseDTO 100 100%
app / dto / user_addiction_dto.py(no class) 1500 100%
app / models / __init__.py(no class) 000 100%
app / models / addiction.pyAddiction 210 50%
app / models / addiction.py(no class) 900 100%
app / models / daily_log.pyDailyLog 220 0%
app / models / daily_log.py(no class) 1000 100%
app / models / user.pyUser 210 50%
app / models / user.py(no class) 1100 100%
app / models / user_addiction.pyUserAddiction 220 0%
app / models / user_addiction.py(no class) 1100 100%
app / routes / __init__.py(no class) 000 100%
app / routes / addictions.py(no class) 1120 82%
app / routes / authorization.py(no class) 55240 56%
app / routes / dashboard.py(no class) 1420 86%
app / routes / days.py(no class) 2720 93%
app / routes / progress.py(no class) 1420 86%
app / routes / settings.py(no class) 31210 32%
app / routes / user_addiction.py(no class) 2620 92%
app / schemas / __init__.py(no class) 000 100%
app / schemas / addiction_schema.pyAddictionItemSchema 000 100%
app / schemas / addiction_schema.pyAddictionListResponseSchema 000 100%
app / schemas / addiction_schema.pyAddictionListResponseSchema.Meta 000 100%
app / schemas / addiction_schema.py(no class) 900 100%
app / schemas / dashboard_schema.pyDayStatusSchema 000 100%
app / schemas / dashboard_schema.pyDashboardResponseSchema 000 100%
app / schemas / dashboard_schema.py(no class) 1000 100%
app / schemas / days_schema.pyUpdateDayStatusRequestSchema 000 100%
app / schemas / days_schema.pyDayEntryResponseSchema 000 100%
app / schemas / days_schema.py(no class) 700 100%
app / schemas / progress_schema.pyDayEntrySchema 000 100%
app / schemas / progress_schema.pyProgressResponseSchema 000 100%
app / schemas / progress_schema.py(no class) 1000 100%
app / schemas / user_addiction_schema.pyAddUserAddictionRequestSchema 000 100%
app / schemas / user_addiction_schema.pyUserAddictionResponseSchema 000 100%
app / schemas / user_addiction_schema.py(no class) 1830 83%
app / services / __init__.py(no class) 000 100%
app / services / addiction_service.pyAddictionService 900 100%
app / services / addiction_service.py(no class) 1000 100%
app / services / dashboard_service.pyDashboardService 4910 98%
app / services / dashboard_service.py(no class) 2100 100%
app / services / days_service.pyDaysService 4100 100%
app / services / days_service.py(no class) 1500 100%
app / services / progress_service.pyProgressService 3600 100%
app / services / progress_service.py(no class) 1900 100%
app / services / settings_service.pySettingsService 43430 0%
app / services / settings_service.py(no class) 1100 100%
app / services / user_addiction_service.pyUserAddictionService 2300 100%
app / services / user_addiction_service.pyConflictException 000 100%
app / services / user_addiction_service.pyNotFoundException 000 100%
app / services / user_addiction_service.py(no class) 1700 100%
Total  7051110 84%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/backend/htmlcov/coverage_html_cb_dd2e7eb5.js b/backend/htmlcov/coverage_html_cb_dd2e7eb5.js new file mode 100644 index 0000000..6f87174 --- /dev/null +++ b/backend/htmlcov/coverage_html_cb_dd2e7eb5.js @@ -0,0 +1,735 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt + +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// General helpers +function debounce(callback, wait) { + let timeoutId = null; + return function(...args) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + callback.apply(this, args); + }, wait); + }; +}; + +function checkVisible(element) { + const rect = element.getBoundingClientRect(); + const viewBottom = Math.max(document.documentElement.clientHeight, window.innerHeight); + const viewTop = 30; + return !(rect.bottom < viewTop || rect.top >= viewBottom); +} + +function on_click(sel, fn) { + const elt = document.querySelector(sel); + if (elt) { + elt.addEventListener("click", fn); + } +} + +// Helpers for table sorting +function getCellValue(row, column = 0) { + const cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (cell.childElementCount == 1) { + var child = cell.firstElementChild; + if (child.tagName === "A") { + child = child.firstElementChild; + } + if (child instanceof HTMLDataElement && child.value) { + return child.value; + } + } + return cell.innerText || cell.textContent; +} + +function rowComparator(rowA, rowB, column = 0) { + let valueA = getCellValue(rowA, column); + let valueB = getCellValue(rowB, column); + if (!isNaN(valueA) && !isNaN(valueB)) { + return valueA - valueB; + } + return valueA.localeCompare(valueB, undefined, {numeric: true}); +} + +function sortColumn(th) { + // Get the current sorting direction of the selected header, + // clear state on other headers and then set the new sorting direction. + const currentSortOrder = th.getAttribute("aria-sort"); + [...th.parentElement.cells].forEach(header => header.setAttribute("aria-sort", "none")); + var direction; + if (currentSortOrder === "none") { + direction = th.dataset.defaultSortOrder || "ascending"; + } + else if (currentSortOrder === "ascending") { + direction = "descending"; + } + else { + direction = "ascending"; + } + th.setAttribute("aria-sort", direction); + + const column = [...th.parentElement.cells].indexOf(th) + + // Sort all rows and afterwards append them in order to move them in the DOM. + Array.from(th.closest("table").querySelectorAll("tbody tr")) + .sort((rowA, rowB) => rowComparator(rowA, rowB, column) * (direction === "ascending" ? 1 : -1)) + .forEach(tr => tr.parentElement.appendChild(tr)); + + // Save the sort order for next time. + if (th.id !== "region") { + let th_id = "file"; // Sort by file if we don't have a column id + let current_direction = direction; + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + if (stored_list) { + ({th_id, direction} = JSON.parse(stored_list)) + } + localStorage.setItem(coverage.INDEX_SORT_STORAGE, JSON.stringify({ + "th_id": th.id, + "direction": current_direction + })); + if (th.id !== th_id || document.getElementById("region")) { + // Sort column has changed, unset sorting by function or class. + localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({ + "by_region": false, + "region_direction": current_direction + })); + } + } + else { + // Sort column has changed to by function or class, remember that. + localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({ + "by_region": true, + "region_direction": direction + })); + } +} + +// Find all the elements with data-shortcut attribute, and use them to assign a shortcut key. +coverage.assign_shortkeys = function () { + document.querySelectorAll("[data-shortcut]").forEach(element => { + document.addEventListener("keypress", event => { + if (event.target.tagName.toLowerCase() === "input") { + return; // ignore keypress from search filter + } + if (event.key === element.dataset.shortcut) { + element.click(); + } + }); + }); +}; + +// Create the events for the filter box. +coverage.wire_up_filter = function () { + // Populate the filter and hide100 inputs if there are saved values for them. + const saved_filter_value = localStorage.getItem(coverage.FILTER_STORAGE); + if (saved_filter_value) { + document.getElementById("filter").value = saved_filter_value; + } + const saved_hide100_value = localStorage.getItem(coverage.HIDE100_STORAGE); + if (saved_hide100_value) { + document.getElementById("hide100").checked = JSON.parse(saved_hide100_value); + } + + // Cache elements. + const table = document.querySelector("table.index"); + const table_body_rows = table.querySelectorAll("tbody tr"); + const no_rows = document.getElementById("no_rows"); + + const footer = table.tFoot.rows[0]; + const ratio_columns = Array.from(footer.cells).map(cell => Boolean(cell.dataset.ratio)); + + // Observe filter keyevents. + const filter_handler = (event => { + // Keep running total of each metric, first index contains number of shown rows + const totals = ratio_columns.map( + is_ratio => is_ratio ? {"numer": 0, "denom": 0} : 0 + ); + + var text = document.getElementById("filter").value; + // Store filter value + localStorage.setItem(coverage.FILTER_STORAGE, text); + const casefold = (text === text.toLowerCase()); + const hide100 = document.getElementById("hide100").checked; + // Store hide value. + localStorage.setItem(coverage.HIDE100_STORAGE, JSON.stringify(hide100)); + + // Hide / show elements. + table_body_rows.forEach(row => { + var show = false; + // Check the text filter. + for (let column = 0; column < totals.length; column++) { + cell = row.cells[column]; + if (cell.classList.contains("name")) { + var celltext = cell.textContent; + if (casefold) { + celltext = celltext.toLowerCase(); + } + if (celltext.includes(text)) { + show = true; + } + } + } + + // Check the "hide covered" filter. + if (show && hide100) { + const [numer, denom] = row.cells[row.cells.length - 1].dataset.ratio.split(" "); + show = (numer !== denom); + } + + if (!show) { + // hide + row.classList.add("hidden"); + return; + } + + // show + row.classList.remove("hidden"); + totals[0]++; + + for (let column = 0; column < totals.length; column++) { + // Accumulate dynamic totals + cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (cell.matches(".name, .spacer")) { + continue; + } + if (ratio_columns[column] && cell.dataset.ratio) { + // Column stores a ratio + const [numer, denom] = cell.dataset.ratio.split(" "); + totals[column]["numer"] += parseInt(numer, 10); // nosemgrep: eslint.detect-object-injection + totals[column]["denom"] += parseInt(denom, 10); // nosemgrep: eslint.detect-object-injection + } + else { + totals[column] += parseInt(cell.textContent, 10); // nosemgrep: eslint.detect-object-injection + } + } + }); + + // Show placeholder if no rows will be displayed. + if (!totals[0]) { + // Show placeholder, hide table. + no_rows.style.display = "block"; + table.style.display = "none"; + return; + } + + // Hide placeholder, show table. + no_rows.style.display = null; + table.style.display = null; + + // Calculate new dynamic sum values based on visible rows. + for (let column = 0; column < totals.length; column++) { + // Get footer cell element. + const cell = footer.cells[column]; // nosemgrep: eslint.detect-object-injection + if (cell.matches(".name, .spacer")) { + continue; + } + + // Set value into dynamic footer cell element. + if (ratio_columns[column]) { + // Percentage column uses the numerator and denominator, + // and adapts to the number of decimal places. + const match = /\.([0-9]+)/.exec(cell.textContent); + const places = match ? match[1].length : 0; + const { numer, denom } = totals[column]; // nosemgrep: eslint.detect-object-injection + cell.dataset.ratio = `${numer} ${denom}`; + // Check denom to prevent NaN if filtered files contain no statements + cell.textContent = denom + ? `${(numer * 100 / denom).toFixed(places)}%` + : `${(100).toFixed(places)}%`; + } + else { + cell.textContent = totals[column]; // nosemgrep: eslint.detect-object-injection + } + } + }); + + document.getElementById("filter").addEventListener("input", debounce(filter_handler)); + document.getElementById("hide100").addEventListener("input", debounce(filter_handler)); + + // Trigger change event on setup, to force filter on page refresh + // (filter value may still be present). + document.getElementById("filter").dispatchEvent(new Event("input")); + document.getElementById("hide100").dispatchEvent(new Event("input")); +}; +coverage.FILTER_STORAGE = "COVERAGE_FILTER_VALUE"; +coverage.HIDE100_STORAGE = "COVERAGE_HIDE100_VALUE"; + +// Set up the click-to-sort columns. +coverage.wire_up_sorting = function () { + document.querySelectorAll("[data-sortable] th[aria-sort]").forEach( + th => th.addEventListener("click", e => sortColumn(e.target)) + ); + + // Look for a localStorage item containing previous sort settings: + let th_id = "file", direction = "ascending"; + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + if (stored_list) { + ({th_id, direction} = JSON.parse(stored_list)); + } + let by_region = false, region_direction = "ascending"; + const sorted_by_region = localStorage.getItem(coverage.SORTED_BY_REGION); + if (sorted_by_region) { + ({ + by_region, + region_direction + } = JSON.parse(sorted_by_region)); + } + + const region_id = "region"; + if (by_region && document.getElementById(region_id)) { + direction = region_direction; + } + // If we are in a page that has a column with id of "region", sort on + // it if the last sort was by function or class. + let th; + if (document.getElementById(region_id)) { + th = document.getElementById(by_region ? region_id : th_id); + } + else { + th = document.getElementById(th_id); + } + th.setAttribute("aria-sort", direction === "ascending" ? "descending" : "ascending"); + th.click() +}; + +coverage.INDEX_SORT_STORAGE = "COVERAGE_INDEX_SORT_2"; +coverage.SORTED_BY_REGION = "COVERAGE_SORT_REGION"; + +// Loaded on index.html +coverage.index_ready = function () { + coverage.assign_shortkeys(); + coverage.wire_up_filter(); + coverage.wire_up_sorting(); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + + on_click(".button_show_hide_help", coverage.show_hide_help); +}; + +// -- pyfile stuff -- + +coverage.LINE_FILTERS_STORAGE = "COVERAGE_LINE_FILTERS"; + +coverage.pyfile_ready = function () { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === "t") { + document.querySelector(frag).closest(".n").classList.add("highlight"); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } + else { + coverage.set_sel(0); + } + + on_click(".button_toggle_run", coverage.toggle_lines); + on_click(".button_toggle_mis", coverage.toggle_lines); + on_click(".button_toggle_exc", coverage.toggle_lines); + on_click(".button_toggle_par", coverage.toggle_lines); + + on_click(".button_next_chunk", coverage.to_next_chunk_nicely); + on_click(".button_prev_chunk", coverage.to_prev_chunk_nicely); + on_click(".button_top_of_page", coverage.to_top); + on_click(".button_first_chunk", coverage.to_first_chunk); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + on_click(".button_to_index", coverage.to_index); + + on_click(".button_show_hide_help", coverage.show_hide_help); + + coverage.filters = undefined; + try { + coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE); + } catch(err) {} + + if (coverage.filters) { + coverage.filters = JSON.parse(coverage.filters); + } + else { + coverage.filters = {run: false, exc: true, mis: true, par: true}; + } + + for (cls in coverage.filters) { + coverage.set_line_visibilty(cls, coverage.filters[cls]); // nosemgrep: eslint.detect-object-injection + } + + coverage.assign_shortkeys(); + coverage.init_scroll_markers(); + coverage.wire_up_sticky_header(); + + document.querySelectorAll("[id^=ctxs]").forEach( + cbox => cbox.addEventListener("click", coverage.expand_contexts) + ); + + // Rebuild scroll markers when the window height changes. + window.addEventListener("resize", coverage.build_scroll_markers); +}; + +coverage.toggle_lines = function (event) { + const btn = event.target.closest("button"); + const category = btn.value + const show = !btn.classList.contains("show_" + category); + coverage.set_line_visibilty(category, show); + coverage.build_scroll_markers(); + coverage.filters[category] = show; + try { + localStorage.setItem(coverage.LINE_FILTERS_STORAGE, JSON.stringify(coverage.filters)); + } catch(err) {} +}; + +coverage.set_line_visibilty = function (category, should_show) { + const cls = "show_" + category; + const btn = document.querySelector(".button_toggle_" + category); + if (btn) { + if (should_show) { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.add(cls)); + btn.classList.add(cls); + } + else { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.remove(cls)); + btn.classList.remove(cls); + } + } +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return document.getElementById("t" + n)?.closest("p"); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +coverage.to_prev_file = function () { + window.location = document.getElementById("prevFileLink").href; +} + +coverage.to_next_file = function () { + window.location = document.getElementById("nextFileLink").href; +} + +coverage.to_index = function () { + location.href = document.getElementById("indexLink").href; +} + +coverage.show_hide_help = function () { + const helpCheck = document.getElementById("help_panel_state") + helpCheck.checked = !helpCheck.checked; +} + +// Return a string indicating what kind of chunk this line belongs to, +// or null if not a chunk. +coverage.chunk_indicator = function (line_elt) { + const classes = line_elt?.className; + if (!classes) { + return null; + } + const match = classes.match(/\bshow_\w+\b/); + if (!match) { + return null; + } + return match[0]; +}; + +coverage.to_next_chunk = function () { + const c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + var chunk_indicator, probe_line; + while (true) { + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + if (chunk_indicator) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_indicator = chunk_indicator; + while (next_indicator === chunk_indicator) { + probe++; + probe_line = c.line_elt(probe); + next_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + const c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + var chunk_indicator = c.chunk_indicator(probe_line); + while (probe > 1 && !chunk_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_indicator = chunk_indicator; + while (prev_indicator === chunk_indicator) { + probe--; + if (probe <= 0) { + return; + } + probe_line = c.line_elt(probe); + prev_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + const begin = coverage.line_elt(coverage.sel_begin); + const end = coverage.line_elt(coverage.sel_end-1); + + return ( + (checkVisible(begin) ? 1 : 0) + + (checkVisible(end) ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the top line on the screen as selection. + + // This will select the top-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(0, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(1); + } + else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the lowest line on the screen as selection. + + // This will select the bottom-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(document.documentElement.clientHeight-1, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(coverage.lines_len); + } + else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (!probe_line) { + return; + } + var the_indicator = c.chunk_indicator(probe_line); + if (the_indicator) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var indicator = the_indicator; + while (probe > 0 && indicator === the_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + break; + } + indicator = c.chunk_indicator(probe_line); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + indicator = the_indicator; + while (indicator === the_indicator) { + probe++; + probe_line = c.line_elt(probe); + indicator = c.chunk_indicator(probe_line); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + // Highlight the lines in the chunk + document.querySelectorAll("#source .highlight").forEach(e => e.classList.remove("highlight")); + for (let probe = coverage.sel_begin; probe < coverage.sel_end; probe++) { + coverage.line_elt(probe).querySelector(".n").classList.add("highlight"); + } + + coverage.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + const element = coverage.line_elt(coverage.sel_begin); + coverage.scroll_window(element.offsetTop - 60); + } +}; + +coverage.scroll_window = function (to_pos) { + window.scroll({top: to_pos, behavior: "smooth"}); +}; + +coverage.init_scroll_markers = function () { + // Init some variables + coverage.lines_len = document.querySelectorAll("#source > p").length; + + // Build html + coverage.build_scroll_markers(); +}; + +coverage.build_scroll_markers = function () { + const temp_scroll_marker = document.getElementById("scroll_marker") + if (temp_scroll_marker) temp_scroll_marker.remove(); + // Don't build markers if the window has no scroll bar. + if (document.body.scrollHeight <= window.innerHeight) { + return; + } + + const marker_scale = window.innerHeight / document.body.scrollHeight; + const line_height = Math.min(Math.max(3, window.innerHeight / coverage.lines_len), 10); + + let previous_line = -99, last_mark, last_top; + + const scroll_marker = document.createElement("div"); + scroll_marker.id = "scroll_marker"; + document.getElementById("source").querySelectorAll( + "p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par" + ).forEach(element => { + const line_top = Math.floor(element.offsetTop * marker_scale); + const line_number = parseInt(element.querySelector(".n a").id.substr(1)); + + if (line_number === previous_line + 1) { + // If this solid missed block just make previous mark higher. + last_mark.style.height = `${line_top + line_height - last_top}px`; + } + else { + // Add colored line in scroll_marker block. + last_mark = document.createElement("div"); + last_mark.id = `m${line_number}`; + last_mark.classList.add("marker"); + last_mark.style.height = `${line_height}px`; + last_mark.style.top = `${line_top}px`; + scroll_marker.append(last_mark); + last_top = line_top; + } + + previous_line = line_number; + }); + + // Append last to prevent layout calculation + document.body.append(scroll_marker); +}; + +coverage.wire_up_sticky_header = function () { + const header = document.querySelector("header"); + const header_bottom = ( + header.querySelector(".content h2").getBoundingClientRect().top - + header.getBoundingClientRect().top + ); + + function updateHeader() { + if (window.scrollY > header_bottom) { + header.classList.add("sticky"); + } + else { + header.classList.remove("sticky"); + } + } + + window.addEventListener("scroll", updateHeader); + updateHeader(); +}; + +coverage.expand_contexts = function (e) { + var ctxs = e.target.parentNode.querySelector(".ctxs"); + + if (!ctxs.classList.contains("expanded")) { + var ctxs_text = ctxs.textContent; + var width = Number(ctxs_text[0]); + ctxs.textContent = ""; + for (var i = 1; i < ctxs_text.length; i += width) { + key = ctxs_text.substring(i, i + width).trim(); + ctxs.appendChild(document.createTextNode(contexts[key])); + ctxs.appendChild(document.createElement("br")); + } + ctxs.classList.add("expanded"); + } +}; + +document.addEventListener("DOMContentLoaded", () => { + if (document.body.classList.contains("indexfile")) { + coverage.index_ready(); + } + else { + coverage.pyfile_ready(); + } +}); diff --git a/backend/htmlcov/favicon_32_cb_c827f16f.png b/backend/htmlcov/favicon_32_cb_c827f16f.png new file mode 100644 index 0000000..8649f04 Binary files /dev/null and b/backend/htmlcov/favicon_32_cb_c827f16f.png differ diff --git a/backend/htmlcov/function_index.html b/backend/htmlcov/function_index.html new file mode 100644 index 0000000..7185ae8 --- /dev/null +++ b/backend/htmlcov/function_index.html @@ -0,0 +1,971 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 84% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Filefunction statementsmissingexcluded coverage
app / __init__.pycreate_app 2100 100%
app / __init__.py(no function) 800 100%
app / config.py(no function) 1600 100%
app / dto / __init__.py(no function) 000 100%
app / dto / addiction_dto.pyAddictionItemDTO.from_model 100 100%
app / dto / addiction_dto.pyAddictionItemDTO.to_dict 100 100%
app / dto / addiction_dto.pyAddictionListResponseDTO.from_models 200 100%
app / dto / addiction_dto.pyAddictionListResponseDTO.to_dict 110 0%
app / dto / addiction_dto.py(no function) 1600 100%
app / dto / dashboard_dto.pyDayStatusDTO.to_dict 100 100%
app / dto / dashboard_dto.pyDashboardResponseDTO.to_dict 100 100%
app / dto / dashboard_dto.py(no function) 1500 100%
app / dto / days_dto.pyUpdateDayStatusRequest.to_dict 110 0%
app / dto / days_dto.pyDayEntryResponse.to_dict 100 100%
app / dto / days_dto.py(no function) 1100 100%
app / dto / progress_dto.pyDayEntryDTO.to_dict 100 100%
app / dto / progress_dto.pyProgressResponseDTO.to_dict 100 100%
app / dto / progress_dto.py(no function) 1500 100%
app / dto / user_addiction_dto.pyAddUserAddictionRequestDTO.to_dict 110 0%
app / dto / user_addiction_dto.pyUserAddictionResponseDTO.to_dict 100 100%
app / dto / user_addiction_dto.py(no function) 1500 100%
app / models / __init__.py(no function) 000 100%
app / models / addiction.pyAddiction.to_dict 100 100%
app / models / addiction.pyAddiction.__repr__ 110 0%
app / models / addiction.py(no function) 900 100%
app / models / daily_log.pyDailyLog.to_dict 110 0%
app / models / daily_log.pyDailyLog.__repr__ 110 0%
app / models / daily_log.py(no function) 1000 100%
app / models / user.pyUser.to_dict 100 100%
app / models / user.pyUser.__repr__ 110 0%
app / models / user.py(no function) 1100 100%
app / models / user_addiction.pyUserAddiction.to_dict 110 0%
app / models / user_addiction.pyUserAddiction.__repr__ 110 0%
app / models / user_addiction.py(no function) 1100 100%
app / routes / __init__.py(no function) 000 100%
app / routes / addictions.pyget_addictions 620 67%
app / routes / addictions.py(no function) 500 100%
app / routes / authorization.pytoken_required 200 100%
app / routes / authorization.pytoken_required.decorated 1720 88%
app / routes / authorization.pylogin 10100 0%
app / routes / authorization.pyregister 12120 0%
app / routes / authorization.py(no function) 1400 100%
app / routes / dashboard.pyget_dashboard 720 71%
app / routes / dashboard.py(no function) 700 100%
app / routes / days.pyupdate_day_status 2020 90%
app / routes / days.py(no function) 700 100%
app / routes / progress.pyget_progress 720 71%
app / routes / progress.py(no function) 700 100%
app / routes / settings.pyhandle_settings 14140 0%
app / routes / settings.pyreset_settings 770 0%
app / routes / settings.py(no function) 1000 100%
app / routes / user_addiction.pyadd_user_addiction 1720 88%
app / routes / user_addiction.py(no function) 900 100%
app / schemas / __init__.py(no function) 000 100%
app / schemas / addiction_schema.py(no function) 900 100%
app / schemas / dashboard_schema.py(no function) 1000 100%
app / schemas / days_schema.py(no function) 700 100%
app / schemas / progress_schema.py(no function) 1000 100%
app / schemas / user_addiction_schema.pyvalidate_date_not_future 630 50%
app / schemas / user_addiction_schema.py(no function) 1200 100%
app / services / __init__.py(no function) 000 100%
app / services / addiction_service.pyAddictionService._get_all_addictions 500 100%
app / services / addiction_service.pyAddictionService.get_all_addictions_dto 200 100%
app / services / addiction_service.pyAddictionService.serialize_addictions_response 200 100%
app / services / addiction_service.py(no function) 1000 100%
app / services / dashboard_service.pyDashboardService.get_user_dashboard_data 1500 100%
app / services / dashboard_service.pyDashboardService._calculate_total_savings 400 100%
app / services / dashboard_service.pyDashboardService._get_all_days_status 1500 100%
app / services / dashboard_service.pyDashboardService._calculate_daily_streak 1310 92%
app / services / dashboard_service.pyDashboardService.serialize_dashboard_response 200 100%
app / services / dashboard_service.py(no function) 2100 100%
app / services / days_service.pyDaysService.update_day_status 3900 100%
app / services / days_service.pyDaysService.serialize_day_entry_response 200 100%
app / services / days_service.py(no function) 1500 100%
app / services / progress_service.pyProgressService.get_user_progress_data 1500 100%
app / services / progress_service.pyProgressService._calculate_total_savings 400 100%
app / services / progress_service.pyProgressService._get_all_entries 1500 100%
app / services / progress_service.pyProgressService.serialize_progress_response 200 100%
app / services / progress_service.py(no function) 1900 100%
app / services / settings_service.pySettingsService.get_user_settings 10100 0%
app / services / settings_service.pySettingsService.update_user_settings 20200 0%
app / services / settings_service.pySettingsService.reset_user_settings 13130 0%
app / services / settings_service.py(no function) 1100 100%
app / services / user_addiction_service.pyUserAddictionService.add_user_addiction 2100 100%
app / services / user_addiction_service.pyUserAddictionService.serialize_user_addiction_response 200 100%
app / services / user_addiction_service.py(no function) 1700 100%
Total  7051110 84%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/backend/htmlcov/index.html b/backend/htmlcov/index.html new file mode 100644 index 0000000..fa054a1 --- /dev/null +++ b/backend/htmlcov/index.html @@ -0,0 +1,414 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 84% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
File statementsmissingexcluded coverage
app / __init__.py 2900 100%
app / config.py 1600 100%
app / dto / __init__.py 000 100%
app / dto / addiction_dto.py 2110 95%
app / dto / dashboard_dto.py 1700 100%
app / dto / days_dto.py 1310 92%
app / dto / progress_dto.py 1700 100%
app / dto / user_addiction_dto.py 1710 94%
app / models / __init__.py 000 100%
app / models / addiction.py 1110 91%
app / models / daily_log.py 1220 83%
app / models / user.py 1310 92%
app / models / user_addiction.py 1320 85%
app / routes / __init__.py 000 100%
app / routes / addictions.py 1120 82%
app / routes / authorization.py 55240 56%
app / routes / dashboard.py 1420 86%
app / routes / days.py 2720 93%
app / routes / progress.py 1420 86%
app / routes / settings.py 31210 32%
app / routes / user_addiction.py 2620 92%
app / schemas / __init__.py 000 100%
app / schemas / addiction_schema.py 900 100%
app / schemas / dashboard_schema.py 1000 100%
app / schemas / days_schema.py 700 100%
app / schemas / progress_schema.py 1000 100%
app / schemas / user_addiction_schema.py 1830 83%
app / services / __init__.py 000 100%
app / services / addiction_service.py 1900 100%
app / services / dashboard_service.py 7010 99%
app / services / days_service.py 5600 100%
app / services / progress_service.py 5500 100%
app / services / settings_service.py 54430 20%
app / services / user_addiction_service.py 4000 100%
Total 7051110 84%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/backend/htmlcov/keybd_closed_cb_900cfef5.png b/backend/htmlcov/keybd_closed_cb_900cfef5.png new file mode 100644 index 0000000..ba119c4 Binary files /dev/null and b/backend/htmlcov/keybd_closed_cb_900cfef5.png differ diff --git a/backend/htmlcov/status.json b/backend/htmlcov/status.json new file mode 100644 index 0000000..ec2b157 --- /dev/null +++ b/backend/htmlcov/status.json @@ -0,0 +1 @@ +{"note":"This file is an internal implementation detail to speed up HTML report generation. Its format can change at any time. You might be looking for the JSON report: https://coverage.rtfd.io/cmd.html#cmd-json","format":5,"version":"7.13.1","globals":"67c259c196a17845f551e8d610a867a5","files":{"z_5f5a17c013354698___init___py":{"hash":"fda5e17322ea737e729cb16b7a28c62f","index":{"url":"z_5f5a17c013354698___init___py.html","file":"app/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":29,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_5f5a17c013354698_config_py":{"hash":"0ebaa5b3fee95be6ce26830887298a10","index":{"url":"z_5f5a17c013354698_config_py.html","file":"app/config.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":16,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f26a397b16319712___init___py":{"hash":"b3b9cd6e63f8a6175e1228400af7292f","index":{"url":"z_f26a397b16319712___init___py.html","file":"app/dto/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":0,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f26a397b16319712_addiction_dto_py":{"hash":"dc7165d86d27c1c2d0b9022db39b01dc","index":{"url":"z_f26a397b16319712_addiction_dto_py.html","file":"app/dto/addiction_dto.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":21,"n_excluded":0,"n_missing":1,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f26a397b16319712_dashboard_dto_py":{"hash":"364029173075e975ee965c68e9a039d9","index":{"url":"z_f26a397b16319712_dashboard_dto_py.html","file":"app/dto/dashboard_dto.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":17,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f26a397b16319712_days_dto_py":{"hash":"e3b6650673d5b65ede6addbd8d428765","index":{"url":"z_f26a397b16319712_days_dto_py.html","file":"app/dto/days_dto.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":13,"n_excluded":0,"n_missing":1,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f26a397b16319712_progress_dto_py":{"hash":"f2f26768f6fcd296519ada088371f681","index":{"url":"z_f26a397b16319712_progress_dto_py.html","file":"app/dto/progress_dto.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":17,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f26a397b16319712_user_addiction_dto_py":{"hash":"6f9ecb50c763bb8d8dfd1db8340e187e","index":{"url":"z_f26a397b16319712_user_addiction_dto_py.html","file":"app/dto/user_addiction_dto.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":17,"n_excluded":0,"n_missing":1,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_6c0e4b930745278b___init___py":{"hash":"d931797d0618580358301720224bbabc","index":{"url":"z_6c0e4b930745278b___init___py.html","file":"app/models/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":0,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_6c0e4b930745278b_addiction_py":{"hash":"ab81f608de11e992bc8e85adaa3088ba","index":{"url":"z_6c0e4b930745278b_addiction_py.html","file":"app/models/addiction.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":11,"n_excluded":0,"n_missing":1,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_6c0e4b930745278b_daily_log_py":{"hash":"96c6ff65f567f955fca274723e507b29","index":{"url":"z_6c0e4b930745278b_daily_log_py.html","file":"app/models/daily_log.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":12,"n_excluded":0,"n_missing":2,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_6c0e4b930745278b_user_py":{"hash":"08550632bae7432d830e079f2b51c8d7","index":{"url":"z_6c0e4b930745278b_user_py.html","file":"app/models/user.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":13,"n_excluded":0,"n_missing":1,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_6c0e4b930745278b_user_addiction_py":{"hash":"aafa9f01a5430ba4e12a4045314a66bd","index":{"url":"z_6c0e4b930745278b_user_addiction_py.html","file":"app/models/user_addiction.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":13,"n_excluded":0,"n_missing":2,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c6de83248c84ada5___init___py":{"hash":"cd17dfbb96d9f6f10f065b21cf4b1759","index":{"url":"z_c6de83248c84ada5___init___py.html","file":"app/routes/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":0,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c6de83248c84ada5_addictions_py":{"hash":"c11f42c90313e51ec082427b3fa1089f","index":{"url":"z_c6de83248c84ada5_addictions_py.html","file":"app/routes/addictions.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":11,"n_excluded":0,"n_missing":2,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c6de83248c84ada5_authorization_py":{"hash":"fd782c66b30ff1d41cfe585020ca337a","index":{"url":"z_c6de83248c84ada5_authorization_py.html","file":"app/routes/authorization.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":55,"n_excluded":0,"n_missing":24,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c6de83248c84ada5_dashboard_py":{"hash":"da573ccaf8e5f09c665d52c7b08e86ec","index":{"url":"z_c6de83248c84ada5_dashboard_py.html","file":"app/routes/dashboard.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":14,"n_excluded":0,"n_missing":2,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c6de83248c84ada5_days_py":{"hash":"a35976ac0a449bf75b9cf43096cdb283","index":{"url":"z_c6de83248c84ada5_days_py.html","file":"app/routes/days.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":27,"n_excluded":0,"n_missing":2,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c6de83248c84ada5_progress_py":{"hash":"0d6e75e83ee9a9ab405d0410a3197caf","index":{"url":"z_c6de83248c84ada5_progress_py.html","file":"app/routes/progress.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":14,"n_excluded":0,"n_missing":2,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c6de83248c84ada5_settings_py":{"hash":"94390823c68301ac940f1235aa5ea362","index":{"url":"z_c6de83248c84ada5_settings_py.html","file":"app/routes/settings.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":31,"n_excluded":0,"n_missing":21,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c6de83248c84ada5_user_addiction_py":{"hash":"baa447760d9ffe48bf5a5a855c6ddd74","index":{"url":"z_c6de83248c84ada5_user_addiction_py.html","file":"app/routes/user_addiction.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":26,"n_excluded":0,"n_missing":2,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c0f67d75e686303c___init___py":{"hash":"3af77470ea17cdb6b9d26e221d913b38","index":{"url":"z_c0f67d75e686303c___init___py.html","file":"app/schemas/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":0,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c0f67d75e686303c_addiction_schema_py":{"hash":"fe9a2fccf5498b37b3bea01f4ed10c9b","index":{"url":"z_c0f67d75e686303c_addiction_schema_py.html","file":"app/schemas/addiction_schema.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":9,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c0f67d75e686303c_dashboard_schema_py":{"hash":"447469afd1b6f2758daacee94007314e","index":{"url":"z_c0f67d75e686303c_dashboard_schema_py.html","file":"app/schemas/dashboard_schema.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":10,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c0f67d75e686303c_days_schema_py":{"hash":"350ad2f797c419dd1735e92a4d6b840f","index":{"url":"z_c0f67d75e686303c_days_schema_py.html","file":"app/schemas/days_schema.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":7,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c0f67d75e686303c_progress_schema_py":{"hash":"2977e8f5312ae4137f95925fb665b671","index":{"url":"z_c0f67d75e686303c_progress_schema_py.html","file":"app/schemas/progress_schema.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":10,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c0f67d75e686303c_user_addiction_schema_py":{"hash":"c8feb5ab207fee2c2012e869e797422d","index":{"url":"z_c0f67d75e686303c_user_addiction_schema_py.html","file":"app/schemas/user_addiction_schema.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":18,"n_excluded":0,"n_missing":3,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c318f3fa19a49f69___init___py":{"hash":"e104154b294704cbd08ab16ea59ef9de","index":{"url":"z_c318f3fa19a49f69___init___py.html","file":"app/services/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":0,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c318f3fa19a49f69_addiction_service_py":{"hash":"2f437126199ce238076c5b497dc4ee19","index":{"url":"z_c318f3fa19a49f69_addiction_service_py.html","file":"app/services/addiction_service.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":19,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c318f3fa19a49f69_dashboard_service_py":{"hash":"617565d980ac1271a2d5510eb4cafada","index":{"url":"z_c318f3fa19a49f69_dashboard_service_py.html","file":"app/services/dashboard_service.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":70,"n_excluded":0,"n_missing":1,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c318f3fa19a49f69_days_service_py":{"hash":"13add2a7702ce7792564725cb9d97766","index":{"url":"z_c318f3fa19a49f69_days_service_py.html","file":"app/services/days_service.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":56,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c318f3fa19a49f69_progress_service_py":{"hash":"ea4e5429083663a70f0242c59d4ed686","index":{"url":"z_c318f3fa19a49f69_progress_service_py.html","file":"app/services/progress_service.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":55,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c318f3fa19a49f69_settings_service_py":{"hash":"4d59043f3e2d744b2a44ac8d0f70c81c","index":{"url":"z_c318f3fa19a49f69_settings_service_py.html","file":"app/services/settings_service.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":54,"n_excluded":0,"n_missing":43,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c318f3fa19a49f69_user_addiction_service_py":{"hash":"bec3d829975afe0f366f0664a1796e34","index":{"url":"z_c318f3fa19a49f69_user_addiction_service_py.html","file":"app/services/user_addiction_service.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":40,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}}}} \ No newline at end of file diff --git a/backend/htmlcov/style_cb_9ff733b0.css b/backend/htmlcov/style_cb_9ff733b0.css new file mode 100644 index 0000000..5e304ce --- /dev/null +++ b/backend/htmlcov/style_cb_9ff733b0.css @@ -0,0 +1,389 @@ +@charset "UTF-8"; +/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ +/* For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt */ +/* Don't edit this .css file. Edit the .scss file instead! */ +html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } + +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { body { color: #eee; } } + +html > body { font-size: 16px; } + +a:active, a:focus { outline: 2px dashed #007acc; } + +p { font-size: .875em; line-height: 1.4em; } + +table { border-collapse: collapse; } + +td { vertical-align: top; } + +table tr.hidden { display: none !important; } + +p#no_rows { display: none; font-size: 1.15em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +a.nav { text-decoration: none; color: inherit; } + +a.nav:hover { text-decoration: underline; color: inherit; } + +.hidden { display: none; } + +header { background: #f8f8f8; width: 100%; z-index: 2; border-bottom: 1px solid #ccc; } + +@media (prefers-color-scheme: dark) { header { background: black; } } + +@media (prefers-color-scheme: dark) { header { border-color: #333; } } + +header .content { padding: 1rem 3.5rem; } + +header h2 { margin-top: .5em; font-size: 1em; } + +header h2 a.button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header h2 a.button { background: #333; } } + +@media (prefers-color-scheme: dark) { header h2 a.button { border-color: #444; } } + +header h2 a.button.current { border: 2px solid; background: #fff; border-color: #999; cursor: default; } + +@media (prefers-color-scheme: dark) { header h2 a.button.current { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { header h2 a.button.current { border-color: #777; } } + +header p.text { margin: .5em 0 -.5em; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { header p.text { color: #aaa; } } + +header.sticky { position: fixed; left: 0; right: 0; height: 2.5em; } + +header.sticky .text { display: none; } + +header.sticky h1, header.sticky h2 { font-size: 1em; margin-top: 0; display: inline-block; } + +header.sticky .content { padding: 0.5rem 3.5rem; } + +header.sticky .content p { font-size: 1em; } + +header.sticky ~ #source { padding-top: 6.5em; } + +main { position: relative; z-index: 1; } + +footer { margin: 1rem 3.5rem; } + +footer .content { padding: 0; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { footer .content { color: #aaa; } } + +#index { margin: 1rem 0 0 3.5rem; } + +h1 { font-size: 1.25em; display: inline-block; } + +#filter_container { float: right; margin: 0 2em 0 0; line-height: 1.66em; } + +#filter_container #filter { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { #filter_container #filter { border-color: #444; } } + +@media (prefers-color-scheme: dark) { #filter_container #filter { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #filter_container #filter { color: #eee; } } + +#filter_container #filter:focus { border-color: #007acc; } + +#filter_container :disabled ~ label { color: #ccc; } + +@media (prefers-color-scheme: dark) { #filter_container :disabled ~ label { color: #444; } } + +#filter_container label { font-size: .875em; color: #666; } + +@media (prefers-color-scheme: dark) { #filter_container label { color: #aaa; } } + +header button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header button { background: #333; } } + +@media (prefers-color-scheme: dark) { header button { border-color: #444; } } + +header button:active, header button:focus { outline: 2px dashed #007acc; } + +header button.run { background: #eeffee; } + +@media (prefers-color-scheme: dark) { header button.run { background: #373d29; } } + +header button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.run.show_run { background: #373d29; } } + +header button.mis { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { header button.mis { background: #4b1818; } } + +header button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.mis.show_mis { background: #4b1818; } } + +header button.exc { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { header button.exc { background: #333; } } + +header button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.exc.show_exc { background: #333; } } + +header button.par { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { header button.par { background: #650; } } + +header button.par.show_par { background: #ffa; border: 2px solid #bbbb00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.par.show_par { background: #650; } } + +#help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } + +#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } + +#help_panel_wrapper { float: right; position: relative; } + +#keyboard_icon { margin: 5px; } + +#help_panel_state { display: none; } + +#help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; color: #333; } + +#help_panel .keyhelp p { margin-top: .75em; } + +#help_panel .legend { font-style: italic; margin-bottom: 1em; } + +.indexfile #help_panel { width: 25em; } + +.pyfile #help_panel { width: 18em; } + +#help_panel_state:checked ~ #help_panel { display: block; } + +kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; border-radius: 3px; } + +#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } + +#source p { position: relative; white-space: pre; } + +#source p * { box-sizing: border-box; } + +#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; user-select: none; } + +@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } + +#source p .n.highlight { background: #ffdd00; } + +#source p .n a { scroll-margin-top: 6em; text-decoration: none; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } + +#source p .n a:hover { text-decoration: underline; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } + +#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } + +@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } + +#source p .t:hover { background: #f2f2f2; } + +@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } + +#source p .t:hover ~ .r .annotate.long { display: block; } + +#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } + +@media (prefers-color-scheme: dark) { #source p .t .com { color: #6a9955; } } + +#source p .t .key { font-weight: bold; line-height: 1px; } + +#source p .t .str, #source p .t .fst { color: #0451a5; } + +@media (prefers-color-scheme: dark) { #source p .t .str, #source p .t .fst { color: #9cdcfe; } } + +#source p.mis .t { border-left: 0.2em solid #ff0000; } + +#source p.mis.show_mis .t { background: #fdd; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } + +#source p.mis.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } + +#source p.mis.mis2 .t { border-left: 0.2em dotted #ff0000; } + +#source p.mis.mis2.show_mis .t { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { #source p.mis.mis2.show_mis .t { background: #351b1b; } } + +#source p.mis.mis2.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.mis2.show_mis .t:hover { background: #532323; } } + +#source p.run .t { border-left: 0.2em solid #00dd00; } + +#source p.run.show_run .t { background: #dfd; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } + +#source p.run.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } + +#source p.run.run2 .t { border-left: 0.2em dotted #00dd00; } + +#source p.run.run2.show_run .t { background: #eeffee; } + +@media (prefers-color-scheme: dark) { #source p.run.run2.show_run .t { background: #2b2e24; } } + +#source p.run.run2.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.run2.show_run .t:hover { background: #404633; } } + +#source p.exc .t { border-left: 0.2em solid #808080; } + +#source p.exc.show_exc .t { background: #eee; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } + +#source p.exc.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } + +#source p.exc.exc2 .t { border-left: 0.2em dotted #808080; } + +#source p.exc.exc2.show_exc .t { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { #source p.exc.exc2.show_exc .t { background: #292929; } } + +#source p.exc.exc2.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.exc2.show_exc .t:hover { background: #3c3c3c; } } + +#source p.par .t { border-left: 0.2em solid #bbbb00; } + +#source p.par.show_par .t { background: #ffa; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } + +#source p.par.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } + +#source p.par.par2 .t { border-left: 0.2em dotted #bbbb00; } + +#source p.par.par2.show_par .t { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { #source p.par.par2.show_par .t { background: #423a0f; } } + +#source p.par.par2.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.par2.show_par .t:hover { background: #6d5d0c; } } + +#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } + +@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } + +#source p .annotate.short:hover ~ .long { display: block; } + +#source p .annotate.long { width: 30em; right: 2.5em; } + +#source p input { display: none; } + +#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } + +#source p input ~ .r label.ctx::before { content: "â–¶ "; } + +#source p input ~ .r label.ctx:hover { background: #e8f4ff; color: #666; } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } + +#source p input:checked ~ .r label.ctx { background: #d0e8ff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } + +#source p input:checked ~ .r label.ctx::before { content: "â–¼ "; } + +#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } + +#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } + +@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } + +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; } + +@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } + +#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } + +#index table.index { margin-left: -.5em; } + +#index td, #index th { text-align: right; vertical-align: baseline; padding: .25em .5em; border-bottom: 1px solid #eee; } + +@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } + +#index td.name, #index th.name { text-align: left; width: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; min-width: 15em; } + +#index td.left, #index th.left { text-align: left; } + +#index td.spacer, #index th.spacer { border: none; padding: 0; } + +#index td.spacer:hover, #index th.spacer:hover { background: inherit; } + +#index th { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-style: italic; color: #333; border-color: #ccc; cursor: pointer; } + +@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } + +@media (prefers-color-scheme: dark) { #index th { border-color: #444; } } + +#index th:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } + +#index th .arrows { color: #666; font-size: 85%; font-family: sans-serif; font-style: normal; pointer-events: none; } + +#index th[aria-sort="ascending"], #index th[aria-sort="descending"] { white-space: nowrap; background: #eee; padding-left: .5em; } + +@media (prefers-color-scheme: dark) { #index th[aria-sort="ascending"], #index th[aria-sort="descending"] { background: #333; } } + +#index th[aria-sort="ascending"] .arrows::after { content: " â–²"; } + +#index th[aria-sort="descending"] .arrows::after { content: " â–¼"; } + +#index tr.grouphead th { cursor: default; font-style: normal; border-color: #999; } + +@media (prefers-color-scheme: dark) { #index tr.grouphead th { border-color: #777; } } + +#index td.name { font-size: 1.15em; } + +#index td.name a { text-decoration: none; color: inherit; } + +#index td.name .no-noun { font-style: italic; } + +#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-bottom: none; } + +#index tr.region:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index tr.region:hover { background: #333; } } + +#index tr.region:hover td.name { text-decoration: underline; color: inherit; } + +#scroll_marker { position: fixed; z-index: 3; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } + +@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } + +#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } + +@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } diff --git a/backend/htmlcov/z_5f5a17c013354698___init___py.html b/backend/htmlcov/z_5f5a17c013354698___init___py.html new file mode 100644 index 0000000..47b2a28 --- /dev/null +++ b/backend/htmlcov/z_5f5a17c013354698___init___py.html @@ -0,0 +1,146 @@ + + + + + Coverage for app/__init__.py: 100% + + + + + +
+
+

+ Coverage for app / __init__.py: + 100% +

+ +

+ 29 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from flask import Flask 

+

2from flask_sqlalchemy import SQLAlchemy 

+

3from flask_migrate import Migrate 

+

4from flask_cors import CORS 

+

5from app.config import config 

+

6 

+

7db = SQLAlchemy() 

+

8migrate = Migrate() 

+

9 

+

10def create_app(config_name='default'): 

+

11 app = Flask(__name__) 

+

12 app.config.from_object(config[config_name]) 

+

13 

+

14 # Initialize extensions 

+

15 db.init_app(app) 

+

16 migrate.init_app(app, db) 

+

17 CORS(app) 

+

18 

+

19 # Import models to ensure they are registered with SQLAlchemy 

+

20 from app.models import user, addiction, user_addiction, daily_log 

+

21 

+

22 # Register blueprints 

+

23 from app.routes.addictions import addictions_bp 

+

24 app.register_blueprint(addictions_bp) 

+

25 

+

26 from app.routes.authorization import auth_bp 

+

27 app.register_blueprint(auth_bp) 

+

28 

+

29 from app.routes.dashboard import dashboard_bp 

+

30 app.register_blueprint(dashboard_bp) 

+

31 

+

32 from app.routes.progress import progress_bp 

+

33 app.register_blueprint(progress_bp) 

+

34 

+

35 from app.routes.days import days_bp 

+

36 app.register_blueprint(days_bp) 

+

37 

+

38 from app.routes.user_addiction import user_addiction_bp 

+

39 app.register_blueprint(user_addiction_bp) 

+

40 

+

41 from app.routes.settings import settings_bp 

+

42 app.register_blueprint(settings_bp) 

+

43 

+

44 # TODO: Add other blueprints later 

+

45 # from app.routes import users_bp, logs_bp 

+

46 # app.register_blueprint(users_bp) 

+

47 # app.register_blueprint(logs_bp) 

+

48 

+

49 return app 

+
+ + + diff --git a/backend/htmlcov/z_5f5a17c013354698_config_py.html b/backend/htmlcov/z_5f5a17c013354698_config_py.html new file mode 100644 index 0000000..8a45689 --- /dev/null +++ b/backend/htmlcov/z_5f5a17c013354698_config_py.html @@ -0,0 +1,128 @@ + + + + + Coverage for app/config.py: 100% + + + + + +
+
+

+ Coverage for app / config.py: + 100% +

+ +

+ 16 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1import os 

+

2from dotenv import load_dotenv 

+

3 

+

4load_dotenv() 

+

5 

+

6class Config: 

+

7 SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-please-change-in-production' 

+

8 SQLALCHEMY_TRACK_MODIFICATIONS = False 

+

9 

+

10class DevelopmentConfig(Config): 

+

11 DEBUG = True 

+

12 # PostgreSQL dla development 

+

13 SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 

+

14 'postgresql://postgres:postgres@localhost:5432/addiction_tracker_dev' 

+

15 

+

16class TestingConfig(Config): 

+

17 TESTING = True 

+

18 # PostgreSQL dla testów w CI/CD, SQLite lokalnie 

+

19 SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or 'sqlite:///:memory:' 

+

20 

+

21class ProductionConfig(Config): 

+

22 DEBUG = False 

+

23 # PostgreSQL dla produkcji 

+

24 SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') 

+

25 

+

26config = { 

+

27 'development': DevelopmentConfig, 

+

28 'testing': TestingConfig, 

+

29 'production': ProductionConfig, 

+

30 'default': DevelopmentConfig 

+

31} 

+
+ + + diff --git a/backend/htmlcov/z_6c0e4b930745278b___init___py.html b/backend/htmlcov/z_6c0e4b930745278b___init___py.html new file mode 100644 index 0000000..ce4cdf8 --- /dev/null +++ b/backend/htmlcov/z_6c0e4b930745278b___init___py.html @@ -0,0 +1,98 @@ + + + + + Coverage for app/models/__init__.py: 100% + + + + + +
+
+

+ Coverage for app / models / __init__.py: + 100% +

+ +

+ 0 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1# Models package 

+
+ + + diff --git a/backend/htmlcov/z_6c0e4b930745278b_addiction_py.html b/backend/htmlcov/z_6c0e4b930745278b_addiction_py.html new file mode 100644 index 0000000..3694bf4 --- /dev/null +++ b/backend/htmlcov/z_6c0e4b930745278b_addiction_py.html @@ -0,0 +1,118 @@ + + + + + Coverage for app/models/addiction.py: 91% + + + + + +
+
+

+ Coverage for app / models / addiction.py: + 91% +

+ +

+ 11 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from app import db 

+

2 

+

3class Addiction(db.Model): 

+

4 __tablename__ = 'addictions' 

+

5 

+

6 id = db.Column(db.Integer, primary_key=True) 

+

7 name = db.Column(db.String(100), nullable=False) 

+

8 health_risk = db.Column(db.Integer, nullable=False) 

+

9 

+

10 # Relationships 

+

11 user_addictions = db.relationship('UserAddiction', backref='addiction', lazy=True) 

+

12 

+

13 def to_dict(self): 

+

14 return { 

+

15 'id': self.id, 

+

16 'name': self.name, 

+

17 'health_risk': self.health_risk 

+

18 } 

+

19 

+

20 def __repr__(self): 

+

21 return f'<Addiction {self.name}>' 

+
+ + + diff --git a/backend/htmlcov/z_6c0e4b930745278b_daily_log_py.html b/backend/htmlcov/z_6c0e4b930745278b_daily_log_py.html new file mode 100644 index 0000000..458a177 --- /dev/null +++ b/backend/htmlcov/z_6c0e4b930745278b_daily_log_py.html @@ -0,0 +1,119 @@ + + + + + Coverage for app/models/daily_log.py: 83% + + + + + +
+
+

+ Coverage for app / models / daily_log.py: + 83% +

+ +

+ 12 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from app import db 

+

2 

+

3class DailyLog(db.Model): 

+

4 __tablename__ = 'daily_logs' 

+

5 

+

6 id = db.Column(db.Integer, primary_key=True) 

+

7 date = db.Column(db.String(10), nullable=False) # YYYY-MM-DD format 

+

8 relapse = db.Column(db.Integer, nullable=False) # 0=trzeźwy, 1=relaps 

+

9 mood = db.Column(db.Integer, nullable=False) # 1-10 

+

10 users_addiction = db.Column(db.Integer, db.ForeignKey('user_addictions.id'), nullable=False) 

+

11 

+

12 def to_dict(self): 

+

13 return { 

+

14 'id': self.id, 

+

15 'date': self.date, 

+

16 'relapse': self.relapse, 

+

17 'mood': self.mood, 

+

18 'users_addiction': self.users_addiction 

+

19 } 

+

20 

+

21 def __repr__(self): 

+

22 return f'<DailyLog {self.date} UserAddiction:{self.users_addiction}>' 

+
+ + + diff --git a/backend/htmlcov/z_6c0e4b930745278b_user_addiction_py.html b/backend/htmlcov/z_6c0e4b930745278b_user_addiction_py.html new file mode 100644 index 0000000..3c0b868 --- /dev/null +++ b/backend/htmlcov/z_6c0e4b930745278b_user_addiction_py.html @@ -0,0 +1,123 @@ + + + + + Coverage for app/models/user_addiction.py: 85% + + + + + +
+
+

+ Coverage for app / models / user_addiction.py: + 85% +

+ +

+ 13 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from app import db 

+

2 

+

3class UserAddiction(db.Model): 

+

4 __tablename__ = 'user_addictions' 

+

5 

+

6 id = db.Column(db.Integer, primary_key=True) 

+

7 user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) 

+

8 addiction_id = db.Column(db.Integer, db.ForeignKey('addictions.id'), nullable=False) 

+

9 start_date = db.Column(db.Integer, nullable=False) # timestamp 

+

10 cost_per_day = db.Column(db.Integer, nullable=True) 

+

11 

+

12 # Relationships 

+

13 daily_logs = db.relationship('DailyLog', backref='user_addiction_rel', lazy=True, cascade='all, delete-orphan') 

+

14 

+

15 def to_dict(self): 

+

16 return { 

+

17 'id': self.id, 

+

18 'user_id': self.user_id, 

+

19 'addiction_id': self.addiction_id, 

+

20 'start_date': self.start_date, 

+

21 'cost_per_day': self.cost_per_day, 

+

22 'addiction': self.addiction.to_dict() if self.addiction else None 

+

23 } 

+

24 

+

25 def __repr__(self): 

+

26 return f'<UserAddiction User:{self.user_id} Addiction:{self.addiction_id}>' 

+
+ + + diff --git a/backend/htmlcov/z_6c0e4b930745278b_user_py.html b/backend/htmlcov/z_6c0e4b930745278b_user_py.html new file mode 100644 index 0000000..1c560f2 --- /dev/null +++ b/backend/htmlcov/z_6c0e4b930745278b_user_py.html @@ -0,0 +1,121 @@ + + + + + Coverage for app/models/user.py: 92% + + + + + +
+
+

+ Coverage for app / models / user.py: + 92% +

+ +

+ 13 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from datetime import date 

+

2from app import db 

+

3 

+

4class User(db.Model): 

+

5 __tablename__ = 'users' 

+

6 

+

7 id = db.Column(db.Integer, primary_key=True) 

+

8 name = db.Column(db.String(100), nullable=False) 

+

9 password = db.Column(db.String(100), nullable=False) 

+

10 created_at = db.Column(db.Date, default=date.today) 

+

11 

+

12 # Relationships 

+

13 user_addictions = db.relationship('UserAddiction', backref='user', lazy=True, cascade='all, delete-orphan') 

+

14 

+

15 def to_dict(self): 

+

16 return { 

+

17 'id': self.id, 

+

18 'name': self.name, 

+

19 'password': self.password, 

+

20 'created_at': self.created_at.isoformat() if self.created_at else None 

+

21 } 

+

22 

+

23 def __repr__(self): 

+

24 return f'<User {self.name}>' 

+
+ + + diff --git a/backend/htmlcov/z_c0f67d75e686303c___init___py.html b/backend/htmlcov/z_c0f67d75e686303c___init___py.html new file mode 100644 index 0000000..a0376fa --- /dev/null +++ b/backend/htmlcov/z_c0f67d75e686303c___init___py.html @@ -0,0 +1,98 @@ + + + + + Coverage for app/schemas/__init__.py: 100% + + + + + +
+
+

+ Coverage for app / schemas / __init__.py: + 100% +

+ +

+ 0 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1# Schemas package 

+
+ + + diff --git a/backend/htmlcov/z_c0f67d75e686303c_addiction_schema_py.html b/backend/htmlcov/z_c0f67d75e686303c_addiction_schema_py.html new file mode 100644 index 0000000..52b13ff --- /dev/null +++ b/backend/htmlcov/z_c0f67d75e686303c_addiction_schema_py.html @@ -0,0 +1,124 @@ + + + + + Coverage for app/schemas/addiction_schema.py: 100% + + + + + +
+
+

+ Coverage for app / schemas / addiction_schema.py: + 100% +

+ +

+ 9 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from marshmallow import Schema, fields, validate 

+

2 

+

3 

+

4class AddictionItemSchema(Schema): 

+

5 """Schema for serializing a single addiction item.""" 

+

6 id = fields.Integer(required=True, description="Unique identifier for the addiction") 

+

7 name = fields.String( 

+

8 required=True, 

+

9 validate=validate.Length(min=1, max=100), 

+

10 description="Name of the addiction" 

+

11 ) 

+

12 health_risk = fields.Integer( 

+

13 required=True, 

+

14 validate=validate.Range(min=1, max=10), 

+

15 description="Health risk level (1-10 scale)" 

+

16 ) 

+

17 

+

18 

+

19class AddictionListResponseSchema(Schema): 

+

20 """Schema for the complete addiction list response.""" 

+

21 

+

22 class Meta: 

+

23 # This makes the schema return a list directly instead of wrapping in an object 

+

24 unknown = 'exclude' 

+

25 

+

26 # Define as a list of addiction items 

+

27 addictions = fields.List(fields.Nested(AddictionItemSchema), required=True) 

+
+ + + diff --git a/backend/htmlcov/z_c0f67d75e686303c_dashboard_schema_py.html b/backend/htmlcov/z_c0f67d75e686303c_dashboard_schema_py.html new file mode 100644 index 0000000..378a2e9 --- /dev/null +++ b/backend/htmlcov/z_c0f67d75e686303c_dashboard_schema_py.html @@ -0,0 +1,125 @@ + + + + + Coverage for app/schemas/dashboard_schema.py: 100% + + + + + +
+
+

+ Coverage for app / schemas / dashboard_schema.py: + 100% +

+ +

+ 10 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from marshmallow import Schema, fields, validate 

+

2 

+

3 

+

4class DayStatusSchema(Schema): 

+

5 """Schema for serializing a single day status.""" 

+

6 date = fields.String(required=True, description="Date in YYYY-MM-DD format") 

+

7 dayOfWeek = fields.String(required=True, description="Day of week abbreviation") 

+

8 status = fields.String( 

+

9 required=True, 

+

10 validate=validate.OneOf(['success', 'failure', 'none']), 

+

11 description="Status of the day: success, failure, or none" 

+

12 ) 

+

13 

+

14 

+

15class DashboardResponseSchema(Schema): 

+

16 """Schema for the complete dashboard response.""" 

+

17 addictionName = fields.String(required=True, description="Name of the user's addiction") 

+

18 totalSavings = fields.Float(required=True, description="Total money saved in PLN") 

+

19 last7Days = fields.List( 

+

20 fields.Nested(DayStatusSchema), 

+

21 required=True, 

+

22 description="Status of all days from start date to today" 

+

23 ) 

+

24 dailyStreak = fields.Integer( 

+

25 required=True, 

+

26 validate=validate.Range(min=0), 

+

27 description="Current streak of successful days" 

+

28 ) 

+
+ + + diff --git a/backend/htmlcov/z_c0f67d75e686303c_days_schema_py.html b/backend/htmlcov/z_c0f67d75e686303c_days_schema_py.html new file mode 100644 index 0000000..0316ecc --- /dev/null +++ b/backend/htmlcov/z_c0f67d75e686303c_days_schema_py.html @@ -0,0 +1,118 @@ + + + + + Coverage for app/schemas/days_schema.py: 100% + + + + + +
+
+

+ Coverage for app / schemas / days_schema.py: + 100% +

+ +

+ 7 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from marshmallow import Schema, fields, validate 

+

2 

+

3 

+

4class UpdateDayStatusRequestSchema(Schema): 

+

5 """Schema for update day status request.""" 

+

6 status = fields.String( 

+

7 required=True, 

+

8 validate=validate.OneOf(['success', 'failure', 'none']), 

+

9 description="Status of the day: success, failure, or none" 

+

10 ) 

+

11 

+

12 

+

13class DayEntryResponseSchema(Schema): 

+

14 """Schema for a single day entry response.""" 

+

15 date = fields.String(required=True, description="Date in YYYY-MM-DD format") 

+

16 dayOfWeek = fields.String(required=True, description="Day of week abbreviation") 

+

17 status = fields.String( 

+

18 required=True, 

+

19 validate=validate.OneOf(['success', 'failure', 'none']), 

+

20 description="Status of the day: success, failure, or none" 

+

21 ) 

+
+ + + diff --git a/backend/htmlcov/z_c0f67d75e686303c_progress_schema_py.html b/backend/htmlcov/z_c0f67d75e686303c_progress_schema_py.html new file mode 100644 index 0000000..6ac587d --- /dev/null +++ b/backend/htmlcov/z_c0f67d75e686303c_progress_schema_py.html @@ -0,0 +1,121 @@ + + + + + Coverage for app/schemas/progress_schema.py: 100% + + + + + +
+
+

+ Coverage for app / schemas / progress_schema.py: + 100% +

+ +

+ 10 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from marshmallow import Schema, fields, validate 

+

2 

+

3 

+

4class DayEntrySchema(Schema): 

+

5 """Schema for serializing a single day entry.""" 

+

6 date = fields.String(required=True, description="Date in YYYY-MM-DD format") 

+

7 dayOfWeek = fields.String(required=True, description="Day of week abbreviation") 

+

8 status = fields.String( 

+

9 required=True, 

+

10 validate=validate.OneOf(['success', 'failure', 'none']), 

+

11 description="Status of the day: success, failure, or none" 

+

12 ) 

+

13 

+

14 

+

15class ProgressResponseSchema(Schema): 

+

16 """Schema for the complete progress response.""" 

+

17 addictionName = fields.String(required=True, description="Name of the user's addiction") 

+

18 singleDayCost = fields.Float(required=True, description="Daily cost of the habit in PLN") 

+

19 totalSavings = fields.Float(required=True, description="Total money saved in PLN") 

+

20 entries = fields.List( 

+

21 fields.Nested(DayEntrySchema), 

+

22 required=True, 

+

23 description="Full history of day entries" 

+

24 ) 

+
+ + + diff --git a/backend/htmlcov/z_c0f67d75e686303c_user_addiction_schema_py.html b/backend/htmlcov/z_c0f67d75e686303c_user_addiction_schema_py.html new file mode 100644 index 0000000..157774c --- /dev/null +++ b/backend/htmlcov/z_c0f67d75e686303c_user_addiction_schema_py.html @@ -0,0 +1,137 @@ + + + + + Coverage for app/schemas/user_addiction_schema.py: 83% + + + + + +
+
+

+ Coverage for app / schemas / user_addiction_schema.py: + 83% +

+ +

+ 18 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from marshmallow import Schema, fields, validate, ValidationError 

+

2from datetime import datetime 

+

3 

+

4 

+

5def validate_date_not_future(date_str): 

+

6 """Validate that date is not in the future.""" 

+

7 try: 

+

8 date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() 

+

9 if date_obj > datetime.now().date(): 

+

10 raise ValidationError("Start date cannot be in the future") 

+

11 except ValueError: 

+

12 raise ValidationError("Invalid date format. Expected YYYY-MM-DD") 

+

13 

+

14 

+

15class AddUserAddictionRequestSchema(Schema): 

+

16 """Schema for validating add user addiction request.""" 

+

17 addictionName = fields.String( 

+

18 required=True, 

+

19 validate=validate.Length(min=1, max=100), 

+

20 description="Name of the addiction" 

+

21 ) 

+

22 startDate = fields.String( 

+

23 required=True, 

+

24 validate=validate_date_not_future, 

+

25 description="Start date in YYYY-MM-DD format" 

+

26 ) 

+

27 costPerDay = fields.Float( 

+

28 required=False, 

+

29 allow_none=True, 

+

30 validate=validate.Range(min=0), 

+

31 description="Daily cost in PLN" 

+

32 ) 

+

33 

+

34 

+

35class UserAddictionResponseSchema(Schema): 

+

36 """Schema for user addiction response.""" 

+

37 id = fields.Integer(required=True, description="User addiction ID") 

+

38 addictionName = fields.String(required=True, description="Name of the addiction") 

+

39 startDate = fields.String(required=True, description="Start date in YYYY-MM-DD format") 

+

40 costPerDay = fields.Float(allow_none=True, description="Daily cost in PLN") 

+
+ + + diff --git a/backend/htmlcov/z_c318f3fa19a49f69___init___py.html b/backend/htmlcov/z_c318f3fa19a49f69___init___py.html new file mode 100644 index 0000000..a6b8c90 --- /dev/null +++ b/backend/htmlcov/z_c318f3fa19a49f69___init___py.html @@ -0,0 +1,98 @@ + + + + + Coverage for app/services/__init__.py: 100% + + + + + +
+
+

+ Coverage for app / services / __init__.py: + 100% +

+ +

+ 0 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1# Services package 

+
+ + + diff --git a/backend/htmlcov/z_c318f3fa19a49f69_addiction_service_py.html b/backend/htmlcov/z_c318f3fa19a49f69_addiction_service_py.html new file mode 100644 index 0000000..e4891fb --- /dev/null +++ b/backend/htmlcov/z_c318f3fa19a49f69_addiction_service_py.html @@ -0,0 +1,147 @@ + + + + + Coverage for app/services/addiction_service.py: 100% + + + + + +
+
+

+ Coverage for app / services / addiction_service.py: + 100% +

+ +

+ 19 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from app.models.addiction import Addiction 

+

2from app.dto.addiction_dto import AddictionListResponseDTO 

+

3from app.schemas.addiction_schema import AddictionItemSchema 

+

4 

+

5class AddictionService: 

+

6 

+

7 @staticmethod 

+

8 def _get_all_addictions(): 

+

9 """ 

+

10 Private method to retrieve all addictions from the database. 

+

11 

+

12 Returns: 

+

13 list: List of Addiction objects 

+

14 

+

15 Raises: 

+

16 Exception: Database error 

+

17 """ 

+

18 try: 

+

19 addictions = Addiction.query.all() 

+

20 return addictions 

+

21 except Exception as e: 

+

22 raise Exception(f"Database error: {str(e)}") 

+

23 

+

24 @staticmethod 

+

25 def get_all_addictions_dto(): 

+

26 """ 

+

27 Retrieves all addictions and returns them as DTOs. 

+

28 

+

29 Returns: 

+

30 AddictionListResponseDTO: DTO containing list of addictions 

+

31 

+

32 Raises: 

+

33 Exception: Database error 

+

34 """ 

+

35 addictions = AddictionService._get_all_addictions() 

+

36 return AddictionListResponseDTO.from_models(addictions) 

+

37 

+

38 @staticmethod 

+

39 def serialize_addictions_response(addiction_list_dto): 

+

40 """ 

+

41 Serializes addiction DTOs using Marshmallow schema. 

+

42 

+

43 Args: 

+

44 addiction_list_dto (AddictionListResponseDTO): DTO to serialize 

+

45 

+

46 Returns: 

+

47 list: Serialized addiction data ready for JSON response 

+

48 """ 

+

49 schema = AddictionItemSchema(many=True) 

+

50 return schema.dump([dto.to_dict() for dto in addiction_list_dto.addictions]) 

+
+ + + diff --git a/backend/htmlcov/z_c318f3fa19a49f69_dashboard_service_py.html b/backend/htmlcov/z_c318f3fa19a49f69_dashboard_service_py.html new file mode 100644 index 0000000..4a062fe --- /dev/null +++ b/backend/htmlcov/z_c318f3fa19a49f69_dashboard_service_py.html @@ -0,0 +1,289 @@ + + + + + Coverage for app/services/dashboard_service.py: 99% + + + + + +
+
+

+ Coverage for app / services / dashboard_service.py: + 99% +

+ +

+ 70 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from datetime import datetime, timedelta, date 

+

2from typing import List 

+

3import pytz 

+

4 

+

5from app import db 

+

6from app.models.user import User 

+

7from app.models.user_addiction import UserAddiction 

+

8from app.models.daily_log import DailyLog 

+

9from app.dto.dashboard_dto import DashboardResponseDTO, DayStatusDTO 

+

10from app.schemas.dashboard_schema import DashboardResponseSchema 

+

11 

+

12 

+

13class DashboardService: 

+

14 

+

15 # Polish day abbreviations mapping 

+

16 DAY_ABBREVIATIONS = { 

+

17 0: 'pon', # Monday 

+

18 1: 'wt', # Tuesday 

+

19 2: 'śr', # Wednesday 

+

20 3: 'czw', # Thursday 

+

21 4: 'pt', # Friday 

+

22 5: 'sob', # Saturday 

+

23 6: 'nd' # Sunday 

+

24 } 

+

25 

+

26 @staticmethod 

+

27 def get_user_dashboard_data(username: str, timezone: str = 'Europe/Warsaw') -> DashboardResponseDTO: 

+

28 """ 

+

29 Get dashboard data for a specific user. 

+

30 

+

31 Args: 

+

32 username (str): The username 

+

33 timezone (str): Timezone for date calculations (default: Europe/Warsaw) 

+

34 

+

35 Returns: 

+

36 DashboardResponseDTO: Dashboard data 

+

37 

+

38 Raises: 

+

39 Exception: If user or user addiction not found, or other errors 

+

40 """ 

+

41 try: 

+

42 # Get user 

+

43 user = User.query.filter_by(name=username).first() 

+

44 if not user: 

+

45 raise Exception(f"User '{username}' not found") 

+

46 

+

47 # Get user's addiction (assuming one active addiction per user) 

+

48 user_addiction = UserAddiction.query.filter_by(user_id=user.id).first() 

+

49 if not user_addiction: 

+

50 raise Exception(f"No addiction found for user '{username}'") 

+

51 

+

52 # Get timezone 

+

53 tz = pytz.timezone(timezone) 

+

54 

+

55 # Calculate components 

+

56 addiction_name = user_addiction.addiction.name 

+

57 total_savings = DashboardService._calculate_total_savings(user_addiction) 

+

58 all_days = DashboardService._get_all_days_status(user_addiction, tz) 

+

59 daily_streak = DashboardService._calculate_daily_streak(user_addiction, tz) 

+

60 

+

61 return DashboardResponseDTO( 

+

62 addiction_name=addiction_name, 

+

63 total_savings=total_savings, 

+

64 all_days=all_days, 

+

65 daily_streak=daily_streak 

+

66 ) 

+

67 

+

68 except Exception as e: 

+

69 raise Exception(f"Dashboard service error: {str(e)}") 

+

70 

+

71 @staticmethod 

+

72 def _calculate_total_savings(user_addiction: UserAddiction) -> float: 

+

73 """ 

+

74 Calculate total savings based on successful days and daily cost. 

+

75 

+

76 Args: 

+

77 user_addiction: UserAddiction instance 

+

78 

+

79 Returns: 

+

80 float: Total savings in PLN 

+

81 """ 

+

82 if not user_addiction.cost_per_day: 

+

83 return 0.0 

+

84 

+

85 # Count successful days (relapse = 0) 

+

86 successful_days = db.session.query(DailyLog).filter( 

+

87 DailyLog.users_addiction == user_addiction.id, 

+

88 DailyLog.relapse == 0 

+

89 ).count() 

+

90 

+

91 return successful_days * user_addiction.cost_per_day 

+

92 

+

93 @staticmethod 

+

94 def _get_all_days_status(user_addiction: UserAddiction, tz: pytz.timezone) -> List[DayStatusDTO]: 

+

95 """ 

+

96 Get status for all days from start date to today. 

+

97 

+

98 Args: 

+

99 user_addiction: UserAddiction instance 

+

100 tz: Timezone for date calculations 

+

101 

+

102 Returns: 

+

103 List[DayStatusDTO]: List of day statuses (newest first) 

+

104 """ 

+

105 today = datetime.now(tz).date() 

+

106 # start_date = datetime.fromtimestamp(user_addiction.start_date, tz).date() 

+

107 days_data = [] 

+

108 

+

109 # Calculate number of days from start to today 

+

110 current_date = today 

+

111 

+

112 while current_date >= date(2025, 5, 1): 

+

113 date_str = current_date.strftime('%Y-%m-%d') 

+

114 

+

115 # Get day of week abbreviation 

+

116 day_of_week = DashboardService.DAY_ABBREVIATIONS[current_date.weekday()] 

+

117 

+

118 # Find daily log for this date 

+

119 daily_log = DailyLog.query.filter( 

+

120 DailyLog.users_addiction == user_addiction.id, 

+

121 DailyLog.date == date_str 

+

122 ).first() 

+

123 

+

124 # Determine status 

+

125 if daily_log is None: 

+

126 status = 'none' 

+

127 elif daily_log.relapse == 0: 

+

128 status = 'success' 

+

129 else: # relapse == 1 

+

130 status = 'failure' 

+

131 

+

132 days_data.append(DayStatusDTO( 

+

133 date=date_str, 

+

134 day_of_week=day_of_week, 

+

135 status=status 

+

136 )) 

+

137 

+

138 current_date -= timedelta(days=1) 

+

139 

+

140 return days_data 

+

141 

+

142 @staticmethod 

+

143 def _calculate_daily_streak(user_addiction: UserAddiction, tz: pytz.timezone) -> int: 

+

144 """ 

+

145 Calculate current streak of successful days. 

+

146 

+

147 Args: 

+

148 user_addiction: UserAddiction instance 

+

149 tz: Timezone for date calculations 

+

150 

+

151 Returns: 

+

152 int: Number of consecutive successful days from today backwards 

+

153 """ 

+

154 today = datetime.now(tz).date() 

+

155 streak = 0 

+

156 current_date = today 

+

157 

+

158 while True: 

+

159 date_str = current_date.strftime('%Y-%m-%d') 

+

160 

+

161 # Find daily log for current date 

+

162 daily_log = DailyLog.query.filter( 

+

163 DailyLog.users_addiction == user_addiction.id, 

+

164 DailyLog.date == date_str 

+

165 ).first() 

+

166 

+

167 # If no log or failure, break streak 

+

168 if daily_log is None or daily_log.relapse == 1: 

+

169 break 

+

170 

+

171 # If success, continue streak 

+

172 if daily_log.relapse == 0: 

+

173 streak += 1 

+

174 current_date -= timedelta(days=1) 

+

175 else: 

+

176 break 

+

177 

+

178 return streak 

+

179 

+

180 @staticmethod 

+

181 def serialize_dashboard_response(dashboard_dto: DashboardResponseDTO) -> dict: 

+

182 """ 

+

183 Serialize dashboard DTO using Marshmallow schema. 

+

184 

+

185 Args: 

+

186 dashboard_dto: DashboardResponseDTO to serialize 

+

187 

+

188 Returns: 

+

189 dict: Serialized dashboard data ready for JSON response 

+

190 """ 

+

191 schema = DashboardResponseSchema() 

+

192 return schema.dump(dashboard_dto.to_dict()) 

+
+ + + diff --git a/backend/htmlcov/z_c318f3fa19a49f69_days_service_py.html b/backend/htmlcov/z_c318f3fa19a49f69_days_service_py.html new file mode 100644 index 0000000..28b167a --- /dev/null +++ b/backend/htmlcov/z_c318f3fa19a49f69_days_service_py.html @@ -0,0 +1,233 @@ + + + + + Coverage for app/services/days_service.py: 100% + + + + + +
+
+

+ Coverage for app / services / days_service.py: + 100% +

+ +

+ 56 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from datetime import datetime 

+

2import pytz 

+

3 

+

4from app import db 

+

5from app.models.user import User 

+

6from app.models.user_addiction import UserAddiction 

+

7from app.models.daily_log import DailyLog 

+

8from app.dto.days_dto import DayEntryResponse 

+

9from app.schemas.days_schema import DayEntryResponseSchema 

+

10 

+

11 

+

12class DaysService: 

+

13 

+

14 # Polish day abbreviations mapping 

+

15 DAY_ABBREVIATIONS = { 

+

16 0: 'pon', # Monday 

+

17 1: 'wt', # Tuesday 

+

18 2: 'śr', # Wednesday 

+

19 3: 'czw', # Thursday 

+

20 4: 'pt', # Friday 

+

21 5: 'sob', # Saturday 

+

22 6: 'nd' # Sunday 

+

23 } 

+

24 

+

25 DEFAULT_MOOD = 5 # Default neutral mood value 

+

26 

+

27 @staticmethod 

+

28 def update_day_status(username: str, date_str: str, status: str, timezone: str = 'Europe/Warsaw') -> DayEntryResponse: 

+

29 """ 

+

30 Update status for a specific day. 

+

31 

+

32 Args: 

+

33 username (str): The username 

+

34 date_str (str): Date in YYYY-MM-DD format 

+

35 status (str): Status to set (success, failure, or none) 

+

36 timezone (str): Timezone for date calculations (default: Europe/Warsaw) 

+

37 

+

38 Returns: 

+

39 DayEntryResponse: Updated day entry 

+

40 

+

41 Raises: 

+

42 Exception: If validation fails or user/addiction not found 

+

43 """ 

+

44 try: 

+

45 # Get timezone 

+

46 tz = pytz.timezone(timezone) 

+

47 today = datetime.now(tz).date() 

+

48 

+

49 # Parse and validate date 

+

50 try: 

+

51 target_date = datetime.strptime(date_str, '%Y-%m-%d').date() 

+

52 except ValueError: 

+

53 raise Exception("Invalid date format. Expected YYYY-MM-DD") 

+

54 

+

55 # Check if date is not in the future 

+

56 if target_date > today: 

+

57 raise Exception("Cannot set status for future dates") 

+

58 

+

59 # Get user 

+

60 user = User.query.filter_by(name=username).first() 

+

61 if not user: 

+

62 raise Exception(f"User '{username}' not found") 

+

63 

+

64 # Get user's addiction 

+

65 user_addiction = UserAddiction.query.filter_by(user_id=user.id).first() 

+

66 if not user_addiction: 

+

67 raise Exception(f"No addiction found for user '{username}'") 

+

68 

+

69 # Find existing daily log for this date 

+

70 daily_log = DailyLog.query.filter( 

+

71 DailyLog.users_addiction == user_addiction.id, 

+

72 DailyLog.date == date_str 

+

73 ).first() 

+

74 

+

75 # Handle status update based on value 

+

76 if status == 'none': 

+

77 # Delete the entry if it exists 

+

78 if daily_log: 

+

79 db.session.delete(daily_log) 

+

80 db.session.commit() 

+

81 elif status == 'success': 

+

82 # Create or update with relapse=0 

+

83 if daily_log: 

+

84 daily_log.relapse = 0 

+

85 daily_log.mood = DaysService.DEFAULT_MOOD 

+

86 else: 

+

87 daily_log = DailyLog( 

+

88 date=date_str, 

+

89 relapse=0, 

+

90 mood=DaysService.DEFAULT_MOOD, 

+

91 users_addiction=user_addiction.id 

+

92 ) 

+

93 db.session.add(daily_log) 

+

94 db.session.commit() 

+

95 elif status == 'failure': 

+

96 # Create or update with relapse=1 

+

97 if daily_log: 

+

98 daily_log.relapse = 1 

+

99 daily_log.mood = DaysService.DEFAULT_MOOD 

+

100 else: 

+

101 daily_log = DailyLog( 

+

102 date=date_str, 

+

103 relapse=1, 

+

104 mood=DaysService.DEFAULT_MOOD, 

+

105 users_addiction=user_addiction.id 

+

106 ) 

+

107 db.session.add(daily_log) 

+

108 db.session.commit() 

+

109 

+

110 # Get day of week abbreviation 

+

111 day_of_week = DaysService.DAY_ABBREVIATIONS[target_date.weekday()] 

+

112 

+

113 # Return the updated entry 

+

114 return DayEntryResponse( 

+

115 date=date_str, 

+

116 day_of_week=day_of_week, 

+

117 status=status 

+

118 ) 

+

119 

+

120 except Exception as e: 

+

121 db.session.rollback() 

+

122 raise Exception(f"Days service error: {str(e)}") 

+

123 

+

124 @staticmethod 

+

125 def serialize_day_entry_response(day_entry: DayEntryResponse) -> dict: 

+

126 """ 

+

127 Serialize day entry DTO using Marshmallow schema. 

+

128 

+

129 Args: 

+

130 day_entry: DayEntryResponse to serialize 

+

131 

+

132 Returns: 

+

133 dict: Serialized day entry data ready for JSON response 

+

134 """ 

+

135 schema = DayEntryResponseSchema() 

+

136 return schema.dump(day_entry.to_dict()) 

+
+ + + diff --git a/backend/htmlcov/z_c318f3fa19a49f69_progress_service_py.html b/backend/htmlcov/z_c318f3fa19a49f69_progress_service_py.html new file mode 100644 index 0000000..3e88483 --- /dev/null +++ b/backend/htmlcov/z_c318f3fa19a49f69_progress_service_py.html @@ -0,0 +1,251 @@ + + + + + Coverage for app/services/progress_service.py: 100% + + + + + +
+
+

+ Coverage for app / services / progress_service.py: + 100% +

+ +

+ 55 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from datetime import datetime, timedelta, date 

+

2from typing import List 

+

3import pytz 

+

4 

+

5from app import db 

+

6from app.models.user import User 

+

7from app.models.user_addiction import UserAddiction 

+

8from app.models.daily_log import DailyLog 

+

9from app.dto.progress_dto import ProgressResponseDTO, DayEntryDTO 

+

10from app.schemas.progress_schema import ProgressResponseSchema 

+

11 

+

12 

+

13class ProgressService: 

+

14 

+

15 # Polish day abbreviations mapping 

+

16 DAY_ABBREVIATIONS = { 

+

17 0: 'pon', # Monday 

+

18 1: 'wt', # Tuesday 

+

19 2: 'śr', # Wednesday 

+

20 3: 'czw', # Thursday 

+

21 4: 'pt', # Friday 

+

22 5: 'sob', # Saturday 

+

23 6: 'nd' # Sunday 

+

24 } 

+

25 

+

26 @staticmethod 

+

27 def get_user_progress_data(username: str, timezone: str = 'Europe/Warsaw') -> ProgressResponseDTO: 

+

28 """ 

+

29 Get progress data for a specific user. 

+

30 

+

31 Args: 

+

32 username (str): The username 

+

33 timezone (str): Timezone for date calculations (default: Europe/Warsaw) 

+

34 

+

35 Returns: 

+

36 ProgressResponseDTO: Progress data 

+

37 

+

38 Raises: 

+

39 Exception: If user or user addiction not found, or other errors 

+

40 """ 

+

41 try: 

+

42 # Get user 

+

43 user = User.query.filter_by(name=username).first() 

+

44 if not user: 

+

45 raise Exception(f"User '{username}' not found") 

+

46 

+

47 # Get user's addiction (assuming one active addiction per user) 

+

48 user_addiction = UserAddiction.query.filter_by(user_id=user.id).first() 

+

49 if not user_addiction: 

+

50 raise Exception(f"No addiction found for user '{username}'") 

+

51 

+

52 # Get timezone 

+

53 tz = pytz.timezone(timezone) 

+

54 

+

55 # Calculate components 

+

56 addiction_name = user_addiction.addiction.name 

+

57 single_day_cost = user_addiction.cost_per_day or 0.0 

+

58 total_savings = ProgressService._calculate_total_savings(user_addiction) 

+

59 entries = ProgressService._get_all_entries(user_addiction, tz) 

+

60 

+

61 return ProgressResponseDTO( 

+

62 addiction_name=addiction_name, 

+

63 single_day_cost=single_day_cost, 

+

64 total_savings=total_savings, 

+

65 entries=entries 

+

66 ) 

+

67 

+

68 except Exception as e: 

+

69 raise Exception(f"Progress service error: {str(e)}") 

+

70 

+

71 @staticmethod 

+

72 def _calculate_total_savings(user_addiction: UserAddiction) -> float: 

+

73 """ 

+

74 Calculate total savings based on successful days and daily cost. 

+

75 

+

76 Args: 

+

77 user_addiction: UserAddiction instance 

+

78 

+

79 Returns: 

+

80 float: Total savings in PLN 

+

81 """ 

+

82 if not user_addiction.cost_per_day: 

+

83 return 0.0 

+

84 

+

85 # Count successful days (relapse = 0) 

+

86 successful_days = db.session.query(DailyLog).filter( 

+

87 DailyLog.users_addiction == user_addiction.id, 

+

88 DailyLog.relapse == 0 

+

89 ).count() 

+

90 

+

91 return successful_days * user_addiction.cost_per_day 

+

92 

+

93 @staticmethod 

+

94 def _get_all_entries(user_addiction: UserAddiction, tz: pytz.timezone) -> List[DayEntryDTO]: 

+

95 """ 

+

96 Get all day entries from start date to today. 

+

97 

+

98 Args: 

+

99 user_addiction: UserAddiction instance 

+

100 tz: Timezone for date calculations 

+

101 

+

102 Returns: 

+

103 List[DayEntryDTO]: List of all day entries (newest first) 

+

104 """ 

+

105 today = datetime.now(tz).date() 

+

106 # start_date = datetime.fromtimestamp(user_addiction.start_date, tz).date() 

+

107 entries = [] 

+

108 

+

109 # Calculate all days from start to today 

+

110 current_date = today 

+

111 

+

112 while current_date >= date(2025, 5, 1): 

+

113 date_str = current_date.strftime('%Y-%m-%d') 

+

114 

+

115 # Get day of week abbreviation 

+

116 day_of_week = ProgressService.DAY_ABBREVIATIONS[current_date.weekday()] 

+

117 

+

118 # Find daily log for this date 

+

119 daily_log = DailyLog.query.filter( 

+

120 DailyLog.users_addiction == user_addiction.id, 

+

121 DailyLog.date == date_str 

+

122 ).first() 

+

123 

+

124 # Determine status 

+

125 if daily_log is None: 

+

126 status = 'none' 

+

127 elif daily_log.relapse == 0: 

+

128 status = 'success' 

+

129 else: # relapse == 1 

+

130 status = 'failure' 

+

131 

+

132 entries.append(DayEntryDTO( 

+

133 date=date_str, 

+

134 day_of_week=day_of_week, 

+

135 status=status 

+

136 )) 

+

137 

+

138 current_date -= timedelta(days=1) 

+

139 

+

140 return entries 

+

141 

+

142 @staticmethod 

+

143 def serialize_progress_response(progress_dto: ProgressResponseDTO) -> dict: 

+

144 """ 

+

145 Serialize progress DTO using Marshmallow schema. 

+

146 

+

147 Args: 

+

148 progress_dto: ProgressResponseDTO to serialize 

+

149 

+

150 Returns: 

+

151 dict: Serialized progress data ready for JSON response 

+

152 """ 

+

153 schema = ProgressResponseSchema() 

+

154 return schema.dump(progress_dto.to_dict()) 

+
+ + + diff --git a/backend/htmlcov/z_c318f3fa19a49f69_settings_service_py.html b/backend/htmlcov/z_c318f3fa19a49f69_settings_service_py.html new file mode 100644 index 0000000..ea71de9 --- /dev/null +++ b/backend/htmlcov/z_c318f3fa19a49f69_settings_service_py.html @@ -0,0 +1,183 @@ + + + + + Coverage for app/services/settings_service.py: 20% + + + + + +
+
+

+ Coverage for app / services / settings_service.py: + 20% +

+ +

+ 54 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from app import db 

+

2from app.models.addiction import Addiction 

+

3from app.models.user import User 

+

4from app.models.user_addiction import UserAddiction 

+

5 

+

6 

+

7class SettingsService: 

+

8 

+

9 @staticmethod 

+

10 def get_user_settings(username): 

+

11 try: 

+

12 user = User.query.filter_by(name=username).first() 

+

13 if not user: 

+

14 raise Exception("User not found") 

+

15 

+

16 # Get the link between user and addiction 

+

17 user_addiction = UserAddiction.query.filter_by(user_id=user.id).first() 

+

18 

+

19 if not user_addiction: 

+

20 return { 

+

21 'username': username, 

+

22 'habitName': None, 

+

23 'habitCost': None 

+

24 } 

+

25 

+

26 return { 

+

27 'username': username, 

+

28 'habitName': user_addiction.addiction.name, 

+

29 'habitCost': user_addiction.cost_per_day 

+

30 } 

+

31 except Exception as e: 

+

32 raise Exception(f"Database error: {str(e)}") 

+

33 

+

34 @staticmethod 

+

35 def update_user_settings(old_username, data): 

+

36 try: 

+

37 user = User.query.filter_by(name=old_username).first() 

+

38 if not user: 

+

39 raise Exception("User not found") 

+

40 

+

41 # 1. Update username if provided 

+

42 new_username = data.get('username') 

+

43 if new_username: 

+

44 user.name = new_username 

+

45 

+

46 # 2. Update Addiction/Cost 

+

47 user_addiction = UserAddiction.query.filter_by(user_id=user.id).first() 

+

48 if user_addiction: 

+

49 # Update cost 

+

50 if 'habitCost' in data: 

+

51 user_addiction.cost_per_day = data['habitCost'] 

+

52 

+

53 if 'habitName' in data: 

+

54 new_habit = Addiction.query.filter_by(name=data['habitName']).first() 

+

55 if new_habit: 

+

56 user_addiction.addiction_id = new_habit.id 

+

57 

+

58 db.session.commit() 

+

59 return { 

+

60 'username': user.name, 

+

61 'habitName': data.get('habitName'), 

+

62 'habitCost': data.get('habitCost') 

+

63 } 

+

64 except Exception as e: 

+

65 db.session.rollback() 

+

66 raise Exception(f"Update error: {str(e)}") 

+

67 

+

68 @staticmethod 

+

69 def reset_user_settings(username): 

+

70 try: 

+

71 user = User.query.filter_by(name=username).first() 

+

72 if not user: 

+

73 raise Exception("User not found") 

+

74 

+

75 # Find the addiction link 

+

76 user_addiction = UserAddiction.query.filter_by(user_id=user.id).first() 

+

77 

+

78 if user_addiction: 

+

79 db.session.delete(user_addiction) 

+

80 db.session.commit() 

+

81 return True 

+

82 

+

83 return False 

+

84 except Exception as e: 

+

85 db.session.rollback() 

+

86 raise Exception(f"Reset error: {str(e)}") 

+
+ + + diff --git a/backend/htmlcov/z_c318f3fa19a49f69_user_addiction_service_py.html b/backend/htmlcov/z_c318f3fa19a49f69_user_addiction_service_py.html new file mode 100644 index 0000000..1e73273 --- /dev/null +++ b/backend/htmlcov/z_c318f3fa19a49f69_user_addiction_service_py.html @@ -0,0 +1,202 @@ + + + + + Coverage for app/services/user_addiction_service.py: 100% + + + + + +
+
+

+ Coverage for app / services / user_addiction_service.py: + 100% +

+ +

+ 40 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from datetime import datetime 

+

2from typing import Optional 

+

3 

+

4from app import db 

+

5from app.models.user import User 

+

6from app.models.addiction import Addiction 

+

7from app.models.user_addiction import UserAddiction 

+

8from app.dto.user_addiction_dto import UserAddictionResponseDTO 

+

9from app.schemas.user_addiction_schema import UserAddictionResponseSchema 

+

10 

+

11 

+

12class UserAddictionService: 

+

13 

+

14 @staticmethod 

+

15 def add_user_addiction( 

+

16 username: str, 

+

17 addiction_name: str, 

+

18 start_date: str, 

+

19 cost_per_day: Optional[float] = None 

+

20 ) -> UserAddictionResponseDTO: 

+

21 """ 

+

22 Add addiction to user. 

+

23 

+

24 Args: 

+

25 username (str): Username from JWT token 

+

26 addiction_name (str): Name of the addiction 

+

27 start_date (str): Start date in YYYY-MM-DD format 

+

28 cost_per_day (Optional[float]): Daily cost in PLN 

+

29 

+

30 Returns: 

+

31 UserAddictionResponseDTO: Created user addiction data 

+

32 

+

33 Raises: 

+

34 Exception: If user not found, addiction not found, or user already has addiction 

+

35 """ 

+

36 try: 

+

37 # 1. Find user 

+

38 user = User.query.filter_by(name=username).first() 

+

39 if not user: 

+

40 raise Exception(f"User '{username}' not found") 

+

41 

+

42 # 2. Check if user already has an addiction 

+

43 existing_addiction = UserAddiction.query.filter_by(user_id=user.id).first() 

+

44 if existing_addiction: 

+

45 raise ConflictException("User already has an assigned addiction") 

+

46 

+

47 # 3. Find addiction by name (case-insensitive) 

+

48 addiction = Addiction.query.filter( 

+

49 db.func.lower(Addiction.name) == addiction_name.lower() 

+

50 ).first() 

+

51 if not addiction: 

+

52 raise NotFoundException(f"Addiction '{addiction_name}' not found") 

+

53 

+

54 # 4. Convert date to Unix timestamp 

+

55 date_obj = datetime.strptime(start_date, '%Y-%m-%d') 

+

56 timestamp = int(date_obj.timestamp()) 

+

57 

+

58 # 5. Create user_addiction record 

+

59 user_addiction = UserAddiction( 

+

60 user_id=user.id, 

+

61 addiction_id=addiction.id, 

+

62 start_date=timestamp, 

+

63 cost_per_day=cost_per_day if cost_per_day is not None else None 

+

64 ) 

+

65 

+

66 db.session.add(user_addiction) 

+

67 db.session.commit() 

+

68 

+

69 # 6. Return DTO 

+

70 return UserAddictionResponseDTO( 

+

71 id=user_addiction.id, 

+

72 addiction_name=addiction.name, 

+

73 start_date=start_date, 

+

74 cost_per_day=cost_per_day 

+

75 ) 

+

76 

+

77 except (ConflictException, NotFoundException): 

+

78 raise 

+

79 except Exception as e: 

+

80 db.session.rollback() 

+

81 raise Exception(f"User addiction service error: {str(e)}") 

+

82 

+

83 @staticmethod 

+

84 def serialize_user_addiction_response(dto: UserAddictionResponseDTO) -> dict: 

+

85 """ 

+

86 Serialize user addiction DTO using Marshmallow schema. 

+

87 

+

88 Args: 

+

89 dto: UserAddictionResponseDTO to serialize 

+

90 

+

91 Returns: 

+

92 dict: Serialized data ready for JSON response 

+

93 """ 

+

94 schema = UserAddictionResponseSchema() 

+

95 return schema.dump(dto.to_dict()) 

+

96 

+

97 

+

98class ConflictException(Exception): 

+

99 """Exception raised when user already has an addiction.""" 

+

100 pass 

+

101 

+

102 

+

103class NotFoundException(Exception): 

+

104 """Exception raised when addiction is not found.""" 

+

105 pass 

+
+ + + diff --git a/backend/htmlcov/z_c6de83248c84ada5___init___py.html b/backend/htmlcov/z_c6de83248c84ada5___init___py.html new file mode 100644 index 0000000..c0dfbbb --- /dev/null +++ b/backend/htmlcov/z_c6de83248c84ada5___init___py.html @@ -0,0 +1,98 @@ + + + + + Coverage for app/routes/__init__.py: 100% + + + + + +
+
+

+ Coverage for app / routes / __init__.py: + 100% +

+ +

+ 0 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1# Routes package 

+
+ + + diff --git a/backend/htmlcov/z_c6de83248c84ada5_addictions_py.html b/backend/htmlcov/z_c6de83248c84ada5_addictions_py.html new file mode 100644 index 0000000..5bf4988 --- /dev/null +++ b/backend/htmlcov/z_c6de83248c84ada5_addictions_py.html @@ -0,0 +1,122 @@ + + + + + Coverage for app/routes/addictions.py: 82% + + + + + +
+
+

+ Coverage for app / routes / addictions.py: + 82% +

+ +

+ 11 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from flask import Blueprint, jsonify 

+

2from app.services.addiction_service import AddictionService 

+

3 

+

4addictions_bp = Blueprint('addictions', __name__) 

+

5 

+

6@addictions_bp.route('/api/addictions', methods=['GET']) 

+

7def get_addictions(): 

+

8 """ 

+

9 Endpoint to retrieve all available addictions. 

+

10 

+

11 Returns: 

+

12 JSON: List of addictions with their details 

+

13 """ 

+

14 try: 

+

15 addiction_list_dto = AddictionService.get_all_addictions_dto() 

+

16 serialized_data = AddictionService.serialize_addictions_response(addiction_list_dto) 

+

17 

+

18 return jsonify(serialized_data), 200 

+

19 

+

20 except Exception as e: 

+

21 return jsonify({ 

+

22 'error': True, 

+

23 'message': str(e), 

+

24 'code': 'ADDICTIONS_FETCH_ERROR' 

+

25 }), 500 

+
+ + + diff --git a/backend/htmlcov/z_c6de83248c84ada5_authorization_py.html b/backend/htmlcov/z_c6de83248c84ada5_authorization_py.html new file mode 100644 index 0000000..a0dd318 --- /dev/null +++ b/backend/htmlcov/z_c6de83248c84ada5_authorization_py.html @@ -0,0 +1,177 @@ + + + + + Coverage for app/routes/authorization.py: 56% + + + + + +
+
+

+ Coverage for app / routes / authorization.py: + 56% +

+ +

+ 55 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1import datetime 

+

2from functools import wraps 

+

3 

+

4import jwt 

+

5from flask import request, jsonify, Blueprint, current_app, g 

+

6from werkzeug.security import check_password_hash, generate_password_hash 

+

7 

+

8from app import db 

+

9from app.models.user import User 

+

10 

+

11auth_bp = Blueprint('auth', __name__, url_prefix='/api') 

+

12 

+

13# Dekorator do ochrony endpointów 

+

14def token_required(f): 

+

15 @wraps(f) 

+

16 def decorated(*args, **kwargs): 

+

17 if request.method == 'OPTIONS': 

+

18 return '', 204 

+

19 token = None 

+

20 if 'Authorization' in request.headers: 

+

21 auth_header = request.headers['Authorization'] 

+

22 if auth_header.startswith("Bearer "): 

+

23 token = auth_header.split(" ")[1] 

+

24 

+

25 if not token: 

+

26 return jsonify({"message": "Token jest wymagany!"}), 401 

+

27 

+

28 try: 

+

29 data = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=["HS256"]) 

+

30 g.current_user = data['username'] 

+

31 

+

32 except jwt.ExpiredSignatureError: 

+

33 return jsonify({"message": "Token wygasl, zaloguj się ponownie"}), 401 

+

34 except jwt.InvalidTokenError: 

+

35 return jsonify({"message": "Token jest nieprawidlowy!"}), 401 

+

36 

+

37 return f(*args, **kwargs) 

+

38 

+

39 return decorated 

+

40 

+

41@auth_bp.route('/login', methods=['POST']) 

+

42def login(): 

+

43 auth = request.json 

+

44 if not auth or not auth.get('username') or not auth.get('password'): 

+

45 return jsonify({"message": "Brak loginu lub hasla"}), 400 

+

46 

+

47 username = auth['username'] 

+

48 password = auth['password'] 

+

49 

+

50 user = User.query.filter_by(name=username).first() 

+

51 

+

52 if user and check_password_hash(user.password, password): 

+

53 token = jwt.encode({ 

+

54 'username': username, 

+

55 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30) 

+

56 }, current_app.config['SECRET_KEY'], algorithm="HS256") 

+

57 return jsonify({'token': token}) 

+

58 

+

59 return jsonify({"message": "Nieprawidlowy login lub haslo"}), 401 

+

60 

+

61 

+

62@auth_bp.route('/register', methods=['POST']) 

+

63def register(): 

+

64 data = request.json 

+

65 username = data.get('username') 

+

66 password = data.get('password') 

+

67 

+

68 if not username or not password: 

+

69 return jsonify({"message": "Brak loginu lub hasla"}), 400 

+

70 

+

71 if User.query.filter_by(name=username).first(): 

+

72 return jsonify({"message": "Uzytkownik juz istnieje"}), 400 

+

73 

+

74 hashed_password = generate_password_hash(password) 

+

75 

+

76 new_user = User(name=username, password=hashed_password) 

+

77 db.session.add(new_user) 

+

78 db.session.commit() 

+

79 

+

80 return jsonify({"message": f"Uzytkownik {username} zostal zarejestrowany!"}), 201 

+
+ + + diff --git a/backend/htmlcov/z_c6de83248c84ada5_dashboard_py.html b/backend/htmlcov/z_c6de83248c84ada5_dashboard_py.html new file mode 100644 index 0000000..71866fa --- /dev/null +++ b/backend/htmlcov/z_c6de83248c84ada5_dashboard_py.html @@ -0,0 +1,132 @@ + + + + + Coverage for app/routes/dashboard.py: 86% + + + + + +
+
+

+ Coverage for app / routes / dashboard.py: + 86% +

+ +

+ 14 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from flask import Blueprint, jsonify, request, g 

+

2from app.services.dashboard_service import DashboardService 

+

3from app.routes.authorization import token_required 

+

4 

+

5dashboard_bp = Blueprint('dashboard', __name__) 

+

6 

+

7@dashboard_bp.route('/api/dashboard', methods=['GET']) 

+

8@token_required 

+

9def get_dashboard(): 

+

10 """ 

+

11 Endpoint to retrieve user dashboard data. 

+

12 

+

13 Returns dashboard data including addiction name, total savings, 

+

14 status of all days (from start date to today), and current success streak. 

+

15 Dates calculated according to Europe/Warsaw timezone (can be overridden with X-Timezone header). 

+

16 

+

17 Returns: 

+

18 JSON: Dashboard data 

+

19 """ 

+

20 try: 

+

21 # Get timezone from header (default: Europe/Warsaw) 

+

22 timezone = request.headers.get('X-Timezone', 'Europe/Warsaw') 

+

23 

+

24 # Get dashboard data 

+

25 dashboard_dto = DashboardService.get_user_dashboard_data(g.current_user, timezone) 

+

26 serialized_data = DashboardService.serialize_dashboard_response(dashboard_dto) 

+

27 

+

28 return jsonify(serialized_data), 200 

+

29 

+

30 except Exception as e: 

+

31 return jsonify({ 

+

32 'error': True, 

+

33 'message': str(e), 

+

34 'code': 'DASHBOARD_FETCH_ERROR' 

+

35 }), 500 

+
+ + + diff --git a/backend/htmlcov/z_c6de83248c84ada5_days_py.html b/backend/htmlcov/z_c6de83248c84ada5_days_py.html new file mode 100644 index 0000000..14055cf --- /dev/null +++ b/backend/htmlcov/z_c6de83248c84ada5_days_py.html @@ -0,0 +1,176 @@ + + + + + Coverage for app/routes/days.py: 93% + + + + + +
+
+

+ Coverage for app / routes / days.py: + 93% +

+ +

+ 27 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from flask import Blueprint, jsonify, request, g 

+

2from app.services.days_service import DaysService 

+

3from app.routes.authorization import token_required 

+

4 

+

5days_bp = Blueprint('days', __name__) 

+

6 

+

7@days_bp.route('/api/days/<string:date>', methods=['PATCH']) 

+

8@token_required 

+

9def update_day_status(date): 

+

10 """ 

+

11 Endpoint to update status for a specific day. 

+

12 

+

13 Updates the status of the day indicated by date (YYYY-MM-DD). 

+

14 Only today's and past dates are allowed. Future dates return 422 Unprocessable Entity. 

+

15 Operation is idempotent - setting the same status returns 200 with current entry. 

+

16 

+

17 Args: 

+

18 date (str): Date in YYYY-MM-DD format 

+

19 

+

20 Returns: 

+

21 JSON: Updated day entry 

+

22 """ 

+

23 try: 

+

24 # Get request data 

+

25 data = request.get_json() 

+

26 if not data or 'status' not in data: 

+

27 return jsonify({ 

+

28 'code': 'VALIDATION_ERROR', 

+

29 'message': 'Status is required', 

+

30 'details': [{'field': 'status', 'issue': 'missing_field'}] 

+

31 }), 400 

+

32 

+

33 status = data['status'] 

+

34 

+

35 # Validate status value 

+

36 if status not in ['success', 'failure', 'none']: 

+

37 return jsonify({ 

+

38 'code': 'VALIDATION_ERROR', 

+

39 'message': 'Invalid status value', 

+

40 'details': [{'field': 'status', 'issue': 'invalid_value', 'value': status}] 

+

41 }), 400 

+

42 

+

43 # Get timezone from header (default: Europe/Warsaw) 

+

44 timezone = request.headers.get('X-Timezone', 'Europe/Warsaw') 

+

45 

+

46 # Update day status 

+

47 day_entry = DaysService.update_day_status(g.current_user, date, status, timezone) 

+

48 serialized_data = DaysService.serialize_day_entry_response(day_entry) 

+

49 

+

50 return jsonify(serialized_data), 200 

+

51 

+

52 except Exception as e: 

+

53 error_message = str(e) 

+

54 

+

55 # Handle specific error cases 

+

56 if 'future' in error_message.lower(): 

+

57 return jsonify({ 

+

58 'code': 'VALIDATION_ERROR', 

+

59 'message': error_message, 

+

60 'details': [{'field': 'date', 'issue': 'future_not_allowed', 'value': date}] 

+

61 }), 422 

+

62 

+

63 if 'not found' in error_message.lower(): 

+

64 return jsonify({ 

+

65 'code': 'NOT_FOUND', 

+

66 'message': error_message 

+

67 }), 404 

+

68 

+

69 if 'invalid date' in error_message.lower(): 

+

70 return jsonify({ 

+

71 'code': 'VALIDATION_ERROR', 

+

72 'message': error_message, 

+

73 'details': [{'field': 'date', 'issue': 'invalid_format', 'value': date}] 

+

74 }), 422 

+

75 

+

76 return jsonify({ 

+

77 'code': 'INTERNAL_ERROR', 

+

78 'message': error_message 

+

79 }), 500 

+
+ + + diff --git a/backend/htmlcov/z_c6de83248c84ada5_progress_py.html b/backend/htmlcov/z_c6de83248c84ada5_progress_py.html new file mode 100644 index 0000000..a99a38c --- /dev/null +++ b/backend/htmlcov/z_c6de83248c84ada5_progress_py.html @@ -0,0 +1,132 @@ + + + + + Coverage for app/routes/progress.py: 86% + + + + + +
+
+

+ Coverage for app / routes / progress.py: + 86% +

+ +

+ 14 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from flask import Blueprint, jsonify, request, g 

+

2from app.services.progress_service import ProgressService 

+

3from app.routes.authorization import token_required 

+

4 

+

5progress_bp = Blueprint('progress', __name__) 

+

6 

+

7@progress_bp.route('/api/progress', methods=['GET']) 

+

8@token_required 

+

9def get_progress(): 

+

10 """ 

+

11 Endpoint to retrieve user progress data. 

+

12 

+

13 Returns full progress history including addiction name, single day cost, 

+

14 total savings, and all day entries from start date to today. 

+

15 Dates calculated according to Europe/Warsaw timezone (can be overridden with X-Timezone header). 

+

16 

+

17 Returns: 

+

18 JSON: Progress data 

+

19 """ 

+

20 try: 

+

21 # Get timezone from header (default: Europe/Warsaw) 

+

22 timezone = request.headers.get('X-Timezone', 'Europe/Warsaw') 

+

23 

+

24 # Get progress data 

+

25 progress_dto = ProgressService.get_user_progress_data(g.current_user, timezone) 

+

26 serialized_data = ProgressService.serialize_progress_response(progress_dto) 

+

27 

+

28 return jsonify(serialized_data), 200 

+

29 

+

30 except Exception as e: 

+

31 return jsonify({ 

+

32 'error': True, 

+

33 'message': str(e), 

+

34 'code': 'PROGRESS_FETCH_ERROR' 

+

35 }), 500 

+
+ + + diff --git a/backend/htmlcov/z_c6de83248c84ada5_settings_py.html b/backend/htmlcov/z_c6de83248c84ada5_settings_py.html new file mode 100644 index 0000000..4b716a0 --- /dev/null +++ b/backend/htmlcov/z_c6de83248c84ada5_settings_py.html @@ -0,0 +1,141 @@ + + + + + Coverage for app/routes/settings.py: 32% + + + + + +
+
+

+ Coverage for app / routes / settings.py: + 32% +

+ +

+ 31 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from flask import Blueprint, jsonify, request, g 

+

2 

+

3from app.routes.authorization import token_required 

+

4from app.services.settings_service import SettingsService 

+

5 

+

6settings_bp = Blueprint('settings', __name__) 

+

7 

+

8@settings_bp.route('/api/settings', methods=['GET', 'PATCH']) 

+

9@token_required 

+

10def handle_settings(): 

+

11 if request.method == 'GET': 

+

12 try: 

+

13 settings_dto = SettingsService.get_user_settings(g.current_user) 

+

14 return jsonify(settings_dto), 200 

+

15 except Exception as e: 

+

16 return jsonify({'error': str(e)}), 500 

+

17 

+

18 if request.method == 'PATCH': 

+

19 try: 

+

20 data = request.get_json() 

+

21 updated_settings = SettingsService.update_user_settings(g.current_user, data) 

+

22 return jsonify(updated_settings), 200 

+

23 except Exception as e: 

+

24 return jsonify({'error': str(e)}), 400 

+

25 return None 

+

26 

+

27@settings_bp.route('/api/settings/reset', methods=['DELETE']) 

+

28@token_required 

+

29def reset_settings(): 

+

30 """ 

+

31 Endpoint to remove user addiction settings. 

+

32 """ 

+

33 try: 

+

34 success = SettingsService.reset_user_settings(g.current_user) 

+

35 if success: 

+

36 return jsonify({'message': 'Ustawienia zostały zresetowane'}), 200 

+

37 else: 

+

38 return jsonify({'message': 'Nie znaleziono ustawień do usunięcia'}), 404 

+

39 

+

40 except Exception as e: 

+

41 return jsonify({ 

+

42 'error': True, 

+

43 'message': str(e) 

+

44 }), 500 

+
+ + + diff --git a/backend/htmlcov/z_c6de83248c84ada5_user_addiction_py.html b/backend/htmlcov/z_c6de83248c84ada5_user_addiction_py.html new file mode 100644 index 0000000..5696a99 --- /dev/null +++ b/backend/htmlcov/z_c6de83248c84ada5_user_addiction_py.html @@ -0,0 +1,177 @@ + + + + + Coverage for app/routes/user_addiction.py: 92% + + + + + +
+
+

+ Coverage for app / routes / user_addiction.py: + 92% +

+ +

+ 26 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from flask import Blueprint, jsonify, request, g 

+

2from marshmallow import ValidationError 

+

3 

+

4from app.services.user_addiction_service import ( 

+

5 UserAddictionService, 

+

6 ConflictException, 

+

7 NotFoundException 

+

8) 

+

9from app.schemas.user_addiction_schema import AddUserAddictionRequestSchema 

+

10from app.routes.authorization import token_required 

+

11 

+

12user_addiction_bp = Blueprint('user_addiction', __name__) 

+

13 

+

14 

+

15@user_addiction_bp.route('/api/user/addiction', methods=['POST']) 

+

16@token_required 

+

17def add_user_addiction(): 

+

18 """ 

+

19 Endpoint to add addiction to user. 

+

20 

+

21 Requires JWT authentication. Creates user_addiction relationship. 

+

22 User can only have one addiction at a time. 

+

23 

+

24 Args: 

+

25 current_user (str): Username from JWT token 

+

26 

+

27 Returns: 

+

28 JSON: Created user addiction data with 201 status 

+

29 """ 

+

30 try: 

+

31 # Validate request body 

+

32 schema = AddUserAddictionRequestSchema() 

+

33 data = schema.load(request.get_json()) 

+

34 

+

35 # Extract data 

+

36 addiction_name = data['addictionName'] 

+

37 start_date = data['startDate'] 

+

38 cost_per_day = data.get('costPerDay') 

+

39 

+

40 # Add user addiction 

+

41 user_addiction_dto = UserAddictionService.add_user_addiction( 

+

42 username=g.current_user, 

+

43 addiction_name=addiction_name, 

+

44 start_date=start_date, 

+

45 cost_per_day=cost_per_day 

+

46 ) 

+

47 

+

48 # Serialize response 

+

49 response_data = UserAddictionService.serialize_user_addiction_response(user_addiction_dto) 

+

50 

+

51 return jsonify(response_data), 201 

+

52 

+

53 except ValidationError as e: 

+

54 return jsonify({ 

+

55 'error': True, 

+

56 'message': 'Validation error', 

+

57 'code': 'VALIDATION_ERROR', 

+

58 'details': e.messages 

+

59 }), 400 

+

60 

+

61 except ConflictException as e: 

+

62 return jsonify({ 

+

63 'error': True, 

+

64 'message': str(e), 

+

65 'code': 'CONFLICT' 

+

66 }), 409 

+

67 

+

68 except NotFoundException as e: 

+

69 return jsonify({ 

+

70 'error': True, 

+

71 'message': str(e), 

+

72 'code': 'NOT_FOUND' 

+

73 }), 404 

+

74 

+

75 except Exception as e: 

+

76 return jsonify({ 

+

77 'error': True, 

+

78 'message': str(e), 

+

79 'code': 'INTERNAL_SERVER_ERROR' 

+

80 }), 500 

+
+ + + diff --git a/backend/htmlcov/z_f26a397b16319712___init___py.html b/backend/htmlcov/z_f26a397b16319712___init___py.html new file mode 100644 index 0000000..afde419 --- /dev/null +++ b/backend/htmlcov/z_f26a397b16319712___init___py.html @@ -0,0 +1,98 @@ + + + + + Coverage for app/dto/__init__.py: 100% + + + + + +
+
+

+ Coverage for app / dto / __init__.py: + 100% +

+ +

+ 0 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1# DTOs package 

+
+ + + diff --git a/backend/htmlcov/z_f26a397b16319712_addiction_dto_py.html b/backend/htmlcov/z_f26a397b16319712_addiction_dto_py.html new file mode 100644 index 0000000..1bcbdcc --- /dev/null +++ b/backend/htmlcov/z_f26a397b16319712_addiction_dto_py.html @@ -0,0 +1,143 @@ + + + + + Coverage for app/dto/addiction_dto.py: 95% + + + + + +
+
+

+ Coverage for app / dto / addiction_dto.py: + 95% +

+ +

+ 21 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from dataclasses import dataclass 

+

2from typing import List 

+

3 

+

4 

+

5@dataclass 

+

6class AddictionItemDTO: 

+

7 """DTO for a single addiction item.""" 

+

8 id: int 

+

9 name: str 

+

10 health_risk: int 

+

11 

+

12 @classmethod 

+

13 def from_model(cls, addiction_model): 

+

14 """Create DTO from Addiction model.""" 

+

15 return cls( 

+

16 id=addiction_model.id, 

+

17 name=addiction_model.name, 

+

18 health_risk=addiction_model.health_risk 

+

19 ) 

+

20 

+

21 def to_dict(self): 

+

22 """Convert DTO to dictionary for JSON serialization.""" 

+

23 return { 

+

24 'id': self.id, 

+

25 'name': self.name, 

+

26 'health_risk': self.health_risk 

+

27 } 

+

28 

+

29 

+

30@dataclass 

+

31class AddictionListResponseDTO: 

+

32 """DTO for the complete addiction list response.""" 

+

33 addictions: List[AddictionItemDTO] 

+

34 

+

35 @classmethod 

+

36 def from_models(cls, addiction_models): 

+

37 """Create response DTO from list of Addiction models.""" 

+

38 addiction_dtos = [ 

+

39 AddictionItemDTO.from_model(model) 

+

40 for model in addiction_models 

+

41 ] 

+

42 return cls(addictions=addiction_dtos) 

+

43 

+

44 def to_dict(self): 

+

45 """Convert to dictionary for JSON serialization.""" 

+

46 return [addiction.to_dict() for addiction in self.addictions] 

+
+ + + diff --git a/backend/htmlcov/z_f26a397b16319712_dashboard_dto_py.html b/backend/htmlcov/z_f26a397b16319712_dashboard_dto_py.html new file mode 100644 index 0000000..b9dbf8b --- /dev/null +++ b/backend/htmlcov/z_f26a397b16319712_dashboard_dto_py.html @@ -0,0 +1,133 @@ + + + + + Coverage for app/dto/dashboard_dto.py: 100% + + + + + +
+
+

+ Coverage for app / dto / dashboard_dto.py: + 100% +

+ +

+ 17 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from dataclasses import dataclass 

+

2from typing import List 

+

3 

+

4 

+

5@dataclass 

+

6class DayStatusDTO: 

+

7 """DTO for a single day status in dashboard.""" 

+

8 date: str 

+

9 day_of_week: str 

+

10 status: str 

+

11 

+

12 def to_dict(self): 

+

13 """Convert DTO to dictionary for JSON serialization.""" 

+

14 return { 

+

15 'date': self.date, 

+

16 'dayOfWeek': self.day_of_week, 

+

17 'status': self.status 

+

18 } 

+

19 

+

20 

+

21@dataclass 

+

22class DashboardResponseDTO: 

+

23 """DTO for the complete dashboard response.""" 

+

24 addiction_name: str 

+

25 total_savings: float 

+

26 all_days: List[DayStatusDTO] 

+

27 daily_streak: int 

+

28 

+

29 def to_dict(self): 

+

30 """Convert to dictionary for JSON serialization.""" 

+

31 return { 

+

32 'addictionName': self.addiction_name, 

+

33 'totalSavings': self.total_savings, 

+

34 'last7Days': [day.to_dict() for day in self.all_days], 

+

35 'dailyStreak': self.daily_streak 

+

36 } 

+
+ + + diff --git a/backend/htmlcov/z_f26a397b16319712_days_dto_py.html b/backend/htmlcov/z_f26a397b16319712_days_dto_py.html new file mode 100644 index 0000000..8059188 --- /dev/null +++ b/backend/htmlcov/z_f26a397b16319712_days_dto_py.html @@ -0,0 +1,126 @@ + + + + + Coverage for app/dto/days_dto.py: 92% + + + + + +
+
+

+ Coverage for app / dto / days_dto.py: + 92% +

+ +

+ 13 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from dataclasses import dataclass 

+

2 

+

3 

+

4@dataclass 

+

5class UpdateDayStatusRequest: 

+

6 """DTO for update day status request.""" 

+

7 status: str 

+

8 

+

9 def to_dict(self): 

+

10 """Convert DTO to dictionary for JSON serialization.""" 

+

11 return { 

+

12 'status': self.status 

+

13 } 

+

14 

+

15 

+

16@dataclass 

+

17class DayEntryResponse: 

+

18 """DTO for a single day entry response.""" 

+

19 date: str 

+

20 day_of_week: str 

+

21 status: str 

+

22 

+

23 def to_dict(self): 

+

24 """Convert DTO to dictionary for JSON serialization.""" 

+

25 return { 

+

26 'date': self.date, 

+

27 'dayOfWeek': self.day_of_week, 

+

28 'status': self.status 

+

29 } 

+
+ + + diff --git a/backend/htmlcov/z_f26a397b16319712_progress_dto_py.html b/backend/htmlcov/z_f26a397b16319712_progress_dto_py.html new file mode 100644 index 0000000..0c91a1d --- /dev/null +++ b/backend/htmlcov/z_f26a397b16319712_progress_dto_py.html @@ -0,0 +1,133 @@ + + + + + Coverage for app/dto/progress_dto.py: 100% + + + + + +
+
+

+ Coverage for app / dto / progress_dto.py: + 100% +

+ +

+ 17 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from dataclasses import dataclass 

+

2from typing import List 

+

3 

+

4 

+

5@dataclass 

+

6class DayEntryDTO: 

+

7 """DTO for a single day entry in progress.""" 

+

8 date: str 

+

9 day_of_week: str 

+

10 status: str 

+

11 

+

12 def to_dict(self): 

+

13 """Convert DTO to dictionary for JSON serialization.""" 

+

14 return { 

+

15 'date': self.date, 

+

16 'dayOfWeek': self.day_of_week, 

+

17 'status': self.status 

+

18 } 

+

19 

+

20 

+

21@dataclass 

+

22class ProgressResponseDTO: 

+

23 """DTO for the complete progress response.""" 

+

24 addiction_name: str 

+

25 single_day_cost: float 

+

26 total_savings: float 

+

27 entries: List[DayEntryDTO] 

+

28 

+

29 def to_dict(self): 

+

30 """Convert to dictionary for JSON serialization.""" 

+

31 return { 

+

32 'addictionName': self.addiction_name, 

+

33 'singleDayCost': self.single_day_cost, 

+

34 'totalSavings': self.total_savings, 

+

35 'entries': [entry.to_dict() for entry in self.entries] 

+

36 } 

+
+ + + diff --git a/backend/htmlcov/z_f26a397b16319712_user_addiction_dto_py.html b/backend/htmlcov/z_f26a397b16319712_user_addiction_dto_py.html new file mode 100644 index 0000000..17e8e84 --- /dev/null +++ b/backend/htmlcov/z_f26a397b16319712_user_addiction_dto_py.html @@ -0,0 +1,133 @@ + + + + + Coverage for app/dto/user_addiction_dto.py: 94% + + + + + +
+
+

+ Coverage for app / dto / user_addiction_dto.py: + 94% +

+ +

+ 17 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.1, + created at 2026-01-10 08:07 +0100 +

+ +
+
+
+

1from dataclasses import dataclass 

+

2from typing import Optional 

+

3 

+

4 

+

5@dataclass 

+

6class AddUserAddictionRequestDTO: 

+

7 """DTO for adding user addiction request.""" 

+

8 addiction_name: str 

+

9 start_date: str 

+

10 cost_per_day: Optional[float] 

+

11 

+

12 def to_dict(self): 

+

13 """Convert DTO to dictionary.""" 

+

14 return { 

+

15 'addictionName': self.addiction_name, 

+

16 'startDate': self.start_date, 

+

17 'costPerDay': self.cost_per_day 

+

18 } 

+

19 

+

20 

+

21@dataclass 

+

22class UserAddictionResponseDTO: 

+

23 """DTO for user addiction response.""" 

+

24 id: int 

+

25 addiction_name: str 

+

26 start_date: str 

+

27 cost_per_day: Optional[float] 

+

28 

+

29 def to_dict(self): 

+

30 """Convert DTO to dictionary for JSON serialization.""" 

+

31 return { 

+

32 'id': self.id, 

+

33 'addictionName': self.addiction_name, 

+

34 'startDate': self.start_date, 

+

35 'costPerDay': self.cost_per_day 

+

36 } 

+
+ + +