diff --git a/backend/htmlcov/.gitignore b/backend/htmlcov/.gitignore new file mode 100644 index 0000000..ccccf14 --- /dev/null +++ b/backend/htmlcov/.gitignore @@ -0,0 +1,2 @@ +# Created by coverage.py +* diff --git a/backend/htmlcov/class_index.html b/backend/htmlcov/class_index.html new file mode 100644 index 0000000..4b659b5 --- /dev/null +++ b/backend/htmlcov/class_index.html @@ -0,0 +1,1187 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 73% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fileclassstatementsmissingexcludedcoverage
comments\__init__.py(no class)000100%
comments\admin.py(no class)300100%
comments\apps.pyCommentsConfig000100%
comments\apps.py(no class)300100%
comments\migrations\0001_initial.pyMigration000100%
comments\migrations\0001_initial.py(no class)700100%
comments\migrations\__init__.py(no class)000100%
comments\models.pyComment000100%
comments\models.pyComment.Meta000100%
comments\models.pyLike000100%
comments\models.py(no class)1900100%
comments\permissions.pyIsCommentVisibleToUser1100%
comments\permissions.py(no class)300100%
comments\serializers.pyCommentSerializer000100%
comments\serializers.pyCommentSerializer.Meta000100%
comments\serializers.pyLikeSerializer000100%
comments\serializers.pyLikeSerializer.Meta000100%
comments\serializers.py(no class)1600100%
comments\urls.py(no class)500100%
comments\views.pyCommentList101000%
comments\views.pyCommentDetail3300%
comments\views.pyLikeList4400%
comments\views.pyLikeDetail3300%
comments\views.py(no class)3900100%
manage.py(no class)112082%
secfit\__init__.py(no class)000100%
secfit\asgi.py(no class)4400%
secfit\settings.py(no class)3000100%
secfit\urls.py(no class)700100%
secfit\wsgi.py(no class)4400%
tests\__init__.py(no class)000100%
tests\test_TC001.pyWorkoutRobustBoundaryTestCase1600100%
tests\test_TC001.py(no class)1000100%
tests\test_TC002.pyAthleteCoachRequestTest3200100%
tests\test_TC002.py(no class)1100100%
tests\test_TC003.pyWorkoutRobustBoundaryTestCase1300100%
tests\test_TC003.py(no class)900100%
tests\test_TC004.pyTestSpecialCharacterFileName1600100%
tests\test_TC004.py(no class)900100%
tests\test_TC005.pyTestCoachViewAthleteWorkouts2000100%
tests\test_TC005.py(no class)800100%
users\__init__.py(no class)000100%
users\admin.pyCustomUserAdmin000100%
users\admin.py(no class)1400100%
users\apps.pyUsersConfig000100%
users\apps.py(no class)300100%
users\auth_backend.pyUsernameAuthBackend101000%
users\auth_backend.py(no class)5500%
users\forms.pyCustomUserCreationForm000100%
users\forms.pyCustomUserCreationForm.Meta000100%
users\forms.pyCustomUserChangeForm000100%
users\forms.pyCustomUserChangeForm.Meta000100%
users\forms.py(no class)1100100%
users\migrations\0001_initial.pyMigration000100%
users\migrations\0001_initial.py(no class)1100100%
users\migrations\0002_user_iscoach.pyMigration000100%
users\migrations\0002_user_iscoach.py(no class)400100%
users\migrations\0003_user_specialism.pyMigration000100%
users\migrations\0003_user_specialism.py(no class)400100%
users\migrations\__init__.py(no class)000100%
users\models.pyUser000100%
users\models.pyAthleteFile000100%
users\models.pyOffer000100%
users\models.py(no class)2300100%
users\permissions.pyIsCurrentUser1100%
users\permissions.pyIsAthlete73057%
users\permissions.pyIsCoach83062%
users\permissions.pyIsRecipientOfOffer8800%
users\permissions.py(no class)1200100%
users\serializers.pyUserSerializer212100%
users\serializers.pyUserSerializer.Meta000100%
users\serializers.pyUserGetSerializer000100%
users\serializers.pyUserGetSerializer.Meta000100%
users\serializers.pyUserPutSerializer3300%
users\serializers.pyUserPutSerializer.Meta000100%
users\serializers.pyAthleteFileSerializer100100%
users\serializers.pyAthleteFileSerializer.Meta000100%
users\serializers.pyOfferSerializer000100%
users\serializers.pyOfferSerializer.Meta000100%
users\serializers.py(no class)3200100%
users\urls.py(no class)400100%
users\validators.pyFileValidator2817039%
users\validators.py(no class)1500100%
users\views.pyUserList9900%
users\views.pyUserDetail343400%
users\views.pyOfferList148043%
users\views.pyOfferDetail206070%
users\views.pyAthleteFileList75029%
users\views.pyAthleteFileDetail2200%
users\views.py(no class)5900100%
workouts\__init__.py(no class)000100%
workouts\admin.py(no class)600100%
workouts\apps.pyWorkoutsConfig000100%
workouts\apps.py(no class)300100%
workouts\migrations\0001_initial.pyMigration000100%
workouts\migrations\0001_initial.py(no class)800100%
workouts\migrations\__init__.py(no class)000100%
workouts\mixins.pyCreateListModelMixin31067%
workouts\mixins.py(no class)200100%
workouts\models.pyOverwriteStorage2200%
workouts\models.pyWorkout1100%
workouts\models.pyWorkout.Meta000100%
workouts\models.pyExercise1100%
workouts\models.pyExerciseInstance000100%
workouts\models.pyWorkoutFile000100%
workouts\models.py(no class)361097%
workouts\parsers.pyMultipartJsonParser171700%
workouts\parsers.py(no class)400100%
workouts\permissions.pyIsOwner1100%
workouts\permissions.pyIsOwnerOfWorkout9900%
workouts\permissions.pyIsCoachAndVisibleToCoach1100%
workouts\permissions.pyIsCoachOfWorkoutAndVisibleToCoach1100%
workouts\permissions.pyIsPublic1100%
workouts\permissions.pyIsWorkoutPublic1100%
workouts\permissions.pyIsReadOnly1100%
workouts\permissions.py(no class)1700100%
workouts\serializers.pyExerciseInstanceSerializer000100%
workouts\serializers.pyExerciseInstanceSerializer.Meta000100%
workouts\serializers.pyWorkoutFileSerializer1100%
workouts\serializers.pyWorkoutFileSerializer.Meta000100%
workouts\serializers.pyWorkoutSerializer4334021%
workouts\serializers.pyWorkoutSerializer.Meta000100%
workouts\serializers.pyExerciseSerializer000100%
workouts\serializers.pyExerciseSerializer.Meta000100%
workouts\serializers.py(no class)3100100%
workouts\urls.py(no class)400100%
workouts\views.pyWorkoutList700100%
workouts\views.pyWorkoutDetail3300%
workouts\views.pyExerciseList2200%
workouts\views.pyExerciseDetail4400%
workouts\views.pyExerciseInstanceList6600%
workouts\views.pyExerciseInstanceDetail4400%
workouts\views.pyWorkoutFileList7700%
workouts\views.pyWorkoutFileDetail2200%
workouts\views.py(no class)791099%
Total 994268073%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/backend/htmlcov/coverage_html_cb_497bf287.js b/backend/htmlcov/coverage_html_cb_497bf287.js new file mode 100644 index 0000000..1face13 --- /dev/null +++ b/backend/htmlcov/coverage_html_cb_497bf287.js @@ -0,0 +1,733 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://github.com/nedbat/coveragepy/blob/master/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"); + + // Observe filter keyevents. + const filter_handler = (event => { + // Keep running total of each metric, first index contains number of shown rows + const totals = new Array(table.rows[0].cells.length).fill(0); + // Accumulate the percentage as fraction + totals[totals.length - 1] = { "numer": 0, "denom": 0 }; // nosemgrep: eslint.detect-object-injection + + 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.classList.contains("name")) { + continue; + } + if (column === totals.length - 1) { + // Last column contains percentage + 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; + + const footer = table.tFoot.rows[0]; + // 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.classList.contains("name")) { + continue; + } + + // Set value into dynamic footer cell element. + if (column === totals.length - 1) { + // 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_58284776.png b/backend/htmlcov/favicon_32_cb_58284776.png new file mode 100644 index 0000000..8649f04 Binary files /dev/null and b/backend/htmlcov/favicon_32_cb_58284776.png differ diff --git a/backend/htmlcov/function_index.html b/backend/htmlcov/function_index.html new file mode 100644 index 0000000..dd6525e --- /dev/null +++ b/backend/htmlcov/function_index.html @@ -0,0 +1,1419 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 73% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Filefunctionstatementsmissingexcludedcoverage
comments\__init__.py(no function)000100%
comments\admin.py(no function)300100%
comments\apps.py(no function)300100%
comments\migrations\0001_initial.py(no function)700100%
comments\migrations\__init__.py(no function)000100%
comments\models.py(no function)1900100%
comments\permissions.pyIsCommentVisibleToUser.has_object_permission1100%
comments\permissions.py(no function)300100%
comments\serializers.py(no function)1600100%
comments\urls.py(no function)500100%
comments\views.pyCommentList.get1100%
comments\views.pyCommentList.post1100%
comments\views.pyCommentList.perform_create1100%
comments\views.pyCommentList.get_queryset7700%
comments\views.pyCommentDetail.get1100%
comments\views.pyCommentDetail.put1100%
comments\views.pyCommentDetail.delete1100%
comments\views.pyLikeList.get1100%
comments\views.pyLikeList.post1100%
comments\views.pyLikeList.perform_create1100%
comments\views.pyLikeList.get_queryset1100%
comments\views.pyLikeDetail.get1100%
comments\views.pyLikeDetail.put1100%
comments\views.pyLikeDetail.delete1100%
comments\views.py(no function)3900100%
manage.pymain62067%
manage.py(no function)500100%
secfit\__init__.py(no function)000100%
secfit\asgi.py(no function)4400%
secfit\settings.py(no function)3000100%
secfit\urls.py(no function)700100%
secfit\wsgi.py(no function)4400%
tests\__init__.py(no function)000100%
tests\test_TC001.pyWorkoutRobustBoundaryTestCase.setUp300100%
tests\test_TC001.pyWorkoutRobustBoundaryTestCase.test_valid_workout_creation500100%
tests\test_TC001.pyWorkoutRobustBoundaryTestCase.test_invalid_workout_creation_future_date400100%
tests\test_TC001.pyWorkoutRobustBoundaryTestCase.test_invalid_workout_creation_past_date400100%
tests\test_TC001.py(no function)1000100%
tests\test_TC002.pyAthleteCoachRequestTest.setUp400100%
tests\test_TC002.pyAthleteCoachRequestTest.test_athlete_initially_has_no_coach100100%
tests\test_TC002.pyAthleteCoachRequestTest.test_athlete_sends_request_to_coach_and_coach_accepts700100%
tests\test_TC002.pyAthleteCoachRequestTest.test_athlete_sends_requests_to_multiple_coaches_and_last_accepting_coach_is_assigned1100100%
tests\test_TC002.pyAthleteCoachRequestTest.test_multiple_requests_to_single_coach_all_other_requests_get_deleted_on_acceptance900100%
tests\test_TC002.py(no function)1100100%
tests\test_TC003.pyWorkoutRobustBoundaryTestCase.setUp300100%
tests\test_TC003.pyWorkoutRobustBoundaryTestCase.test_valid_workout_creation500100%
tests\test_TC003.pyWorkoutRobustBoundaryTestCase.test_invalid_workout_creation500100%
tests\test_TC003.py(no function)900100%
tests\test_TC004.pyTestSpecialCharacterFileName.setUp700100%
tests\test_TC004.pyTestSpecialCharacterFileName.test_upload_file_with_special_characters_in_name900100%
tests\test_TC004.py(no function)900100%
tests\test_TC005.pyTestCoachViewAthleteWorkouts.setUp900100%
tests\test_TC005.pyTestCoachViewAthleteWorkouts.test_coach_can_view_assigned_athlete_workouts1100100%
tests\test_TC005.py(no function)800100%
users\__init__.py(no function)000100%
users\admin.py(no function)1400100%
users\apps.py(no function)300100%
users\auth_backend.pyUsernameAuthBackend.authenticate6600%
users\auth_backend.pyUsernameAuthBackend.get_user4400%
users\auth_backend.py(no function)5500%
users\forms.py(no function)1100100%
users\migrations\0001_initial.py(no function)1100100%
users\migrations\0002_user_iscoach.py(no function)400100%
users\migrations\0003_user_specialism.py(no function)400100%
users\migrations\__init__.py(no function)000100%
users\models.pyathlete_directory_path100100%
users\models.py(no function)2200100%
users\permissions.pyIsCurrentUser.has_object_permission1100%
users\permissions.pyIsAthlete.has_permission62067%
users\permissions.pyIsAthlete.has_object_permission1100%
users\permissions.pyIsCoach.has_permission72071%
users\permissions.pyIsCoach.has_object_permission1100%
users\permissions.pyIsRecipientOfOffer.has_permission8800%
users\permissions.py(no function)1200100%
users\serializers.pyUserSerializer.validate_password101000%
users\serializers.pyUserSerializer.create111100%
users\serializers.pyUserPutSerializer.update3300%
users\serializers.pyAthleteFileSerializer.create100100%
users\serializers.py(no function)3200100%
users\urls.py(no function)400100%
users\validators.pyFileValidator.__init__400100%
users\validators.pyFileValidator.__eq__1100%
users\validators.pyFileValidator.__call__2316030%
users\validators.py(no function)1500100%
users\views.pyUserList.get2200%
users\views.pyUserList.post1100%
users\views.pyUserList.get_queryset6600%
users\views.pyUserDetail.get_object5500%
users\views.pyUserDetail.get252500%
users\views.pyUserDetail.delete1100%
users\views.pyUserDetail.put2200%
users\views.pyUserDetail.patch1100%
users\views.pyOfferList.get1100%
users\views.pyOfferList.post500100%
users\views.pyOfferList.perform_create100100%
users\views.pyOfferList.get_queryset7700%
users\views.pyOfferDetail.get1100%
users\views.pyOfferDetail.put173082%
users\views.pyOfferDetail.patch1100%
users\views.pyOfferDetail.delete1100%
users\views.pyAthleteFileList.get1100%
users\views.pyAthleteFileList.post100100%
users\views.pyAthleteFileList.perform_create100100%
users\views.pyAthleteFileList.get_queryset4400%
users\views.pyAthleteFileDetail.get1100%
users\views.pyAthleteFileDetail.delete1100%
users\views.py(no function)5900100%
workouts\__init__.py(no function)000100%
workouts\admin.py(no function)600100%
workouts\apps.py(no function)300100%
workouts\migrations\0001_initial.py(no function)800100%
workouts\migrations\__init__.py(no function)000100%
workouts\mixins.pyCreateListModelMixin.get_serializer31067%
workouts\mixins.py(no function)200100%
workouts\models.pyOverwriteStorage.get_available_name2200%
workouts\models.pyWorkout.__str__1100%
workouts\models.pyExercise.__str__1100%
workouts\models.pyworkout_directory_path1100%
workouts\models.py(no function)3500100%
workouts\parsers.pyMultipartJsonParser.parse171700%
workouts\parsers.py(no function)400100%
workouts\permissions.pyIsOwner.has_object_permission1100%
workouts\permissions.pyIsOwnerOfWorkout.has_permission8800%
workouts\permissions.pyIsOwnerOfWorkout.has_object_permission1100%
workouts\permissions.pyIsCoachAndVisibleToCoach.has_object_permission1100%
workouts\permissions.pyIsCoachOfWorkoutAndVisibleToCoach.has_object_permission1100%
workouts\permissions.pyIsPublic.has_object_permission1100%
workouts\permissions.pyIsWorkoutPublic.has_object_permission1100%
workouts\permissions.pyIsReadOnly.has_object_permission1100%
workouts\permissions.py(no function)1700100%
workouts\serializers.pyWorkoutFileSerializer.create1100%
workouts\serializers.pyWorkoutSerializer.create102080%
workouts\serializers.pyWorkoutSerializer.update323200%
workouts\serializers.pyWorkoutSerializer.get_owner_username100100%
workouts\serializers.py(no function)3100100%
workouts\urls.py(no function)400100%
workouts\views.pyapi_root1100%
workouts\views.pyWorkoutList.get100100%
workouts\views.pyWorkoutList.post100100%
workouts\views.pyWorkoutList.perform_create100100%
workouts\views.pyWorkoutList.get_queryset400100%
workouts\views.pyWorkoutDetail.get1100%
workouts\views.pyWorkoutDetail.put1100%
workouts\views.pyWorkoutDetail.delete1100%
workouts\views.pyExerciseList.get1100%
workouts\views.pyExerciseList.post1100%
workouts\views.pyExerciseDetail.get1100%
workouts\views.pyExerciseDetail.put1100%
workouts\views.pyExerciseDetail.patch1100%
workouts\views.pyExerciseDetail.delete1100%
workouts\views.pyExerciseInstanceList.get1100%
workouts\views.pyExerciseInstanceList.post1100%
workouts\views.pyExerciseInstanceList.get_queryset4400%
workouts\views.pyExerciseInstanceDetail.get1100%
workouts\views.pyExerciseInstanceDetail.put1100%
workouts\views.pyExerciseInstanceDetail.patch1100%
workouts\views.pyExerciseInstanceDetail.delete1100%
workouts\views.pyWorkoutFileList.get1100%
workouts\views.pyWorkoutFileList.post1100%
workouts\views.pyWorkoutFileList.perform_create1100%
workouts\views.pyWorkoutFileList.get_queryset4400%
workouts\views.pyWorkoutFileDetail.get1100%
workouts\views.pyWorkoutFileDetail.delete1100%
workouts\views.py(no function)7800100%
Total 994268073%
+

+ 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..a228b60 --- /dev/null +++ b/backend/htmlcov/index.html @@ -0,0 +1,447 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 73% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Filestatementsmissingexcludedcoverage
comments\__init__.py000100%
comments\admin.py300100%
comments\apps.py300100%
comments\migrations\0001_initial.py700100%
comments\migrations\__init__.py000100%
comments\models.py1900100%
comments\permissions.py41075%
comments\serializers.py1600100%
comments\urls.py500100%
comments\views.py5920066%
manage.py112082%
secfit\__init__.py000100%
secfit\asgi.py4400%
secfit\settings.py3000100%
secfit\urls.py700100%
secfit\wsgi.py4400%
tests\__init__.py000100%
tests\test_TC001.py2600100%
tests\test_TC002.py4300100%
tests\test_TC003.py2200100%
tests\test_TC004.py2500100%
tests\test_TC005.py2800100%
users\__init__.py000100%
users\admin.py1400100%
users\apps.py300100%
users\auth_backend.py151500%
users\forms.py1100100%
users\migrations\0001_initial.py1100100%
users\migrations\0002_user_iscoach.py400100%
users\migrations\0003_user_specialism.py400100%
users\migrations\__init__.py000100%
users\models.py2300100%
users\permissions.py3615058%
users\serializers.py5724058%
users\urls.py400100%
users\validators.py4317060%
users\views.py14564056%
workouts\__init__.py000100%
workouts\admin.py600100%
workouts\apps.py300100%
workouts\migrations\0001_initial.py800100%
workouts\migrations\__init__.py000100%
workouts\mixins.py51080%
workouts\models.py405088%
workouts\parsers.py2117019%
workouts\permissions.py3215053%
workouts\serializers.py7535053%
workouts\urls.py400100%
workouts\views.py11429075%
Total994268073%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/backend/htmlcov/keybd_closed_cb_ce680311.png b/backend/htmlcov/keybd_closed_cb_ce680311.png new file mode 100644 index 0000000..ba119c4 Binary files /dev/null and b/backend/htmlcov/keybd_closed_cb_ce680311.png differ diff --git a/backend/htmlcov/manage_py.html b/backend/htmlcov/manage_py.html new file mode 100644 index 0000000..9baf6c8 --- /dev/null +++ b/backend/htmlcov/manage_py.html @@ -0,0 +1,119 @@ + + + + + Coverage for manage.py: 82% + + + + + +
+
+

+ Coverage for manage.py: + 82% +

+ +

+ 11 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1#!/usr/bin/env python 

+

2"""Django's command-line utility for administrative tasks.""" 

+

3import os 

+

4import sys 

+

5 

+

6 

+

7def main(): 

+

8 """Run administrative tasks.""" 

+

9 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'secfit.settings') 

+

10 try: 

+

11 from django.core.management import execute_from_command_line 

+

12 except ImportError as exc: 

+

13 raise ImportError( 

+

14 "Couldn't import Django. Are you sure it's installed and " 

+

15 "available on your PYTHONPATH environment variable? Did you " 

+

16 "forget to activate a virtual environment?" 

+

17 ) from exc 

+

18 execute_from_command_line(sys.argv) 

+

19 

+

20 

+

21if __name__ == '__main__': 

+

22 main() 

+
+ + + diff --git a/backend/htmlcov/status.json b/backend/htmlcov/status.json new file mode 100644 index 0000000..99aeb7c --- /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.7.1","globals":"533a7b3d771750494f116c10eeec6819","files":{"z_755815454394ceaf___init___py":{"hash":"3c77fc9ef7f887ac2508d4109cf92472","index":{"url":"z_755815454394ceaf___init___py.html","file":"comments\\__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_755815454394ceaf_admin_py":{"hash":"199548f0847d57c57ca254d2815db27e","index":{"url":"z_755815454394ceaf_admin_py.html","file":"comments\\admin.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":3,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_755815454394ceaf_apps_py":{"hash":"a4c8cebf59b4c72eb88b00c88e53af72","index":{"url":"z_755815454394ceaf_apps_py.html","file":"comments\\apps.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":3,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_66f0b8a779cd646b_0001_initial_py":{"hash":"3ece96cc63c247b9cb2724752381e2f9","index":{"url":"z_66f0b8a779cd646b_0001_initial_py.html","file":"comments\\migrations\\0001_initial.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_66f0b8a779cd646b___init___py":{"hash":"3c77fc9ef7f887ac2508d4109cf92472","index":{"url":"z_66f0b8a779cd646b___init___py.html","file":"comments\\migrations\\__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_755815454394ceaf_models_py":{"hash":"080d5427bdc26ca37f9fc6d6a518b4b9","index":{"url":"z_755815454394ceaf_models_py.html","file":"comments\\models.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_755815454394ceaf_permissions_py":{"hash":"5059ceeaf5266397d72009f0ea12576e","index":{"url":"z_755815454394ceaf_permissions_py.html","file":"comments\\permissions.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":4,"n_excluded":0,"n_missing":1,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_755815454394ceaf_serializers_py":{"hash":"d0bdd820fe47a1e4221ec7ed96284ad1","index":{"url":"z_755815454394ceaf_serializers_py.html","file":"comments\\serializers.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_755815454394ceaf_urls_py":{"hash":"99cb8189f1dd4dcb8299836c5339340e","index":{"url":"z_755815454394ceaf_urls_py.html","file":"comments\\urls.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":5,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_755815454394ceaf_views_py":{"hash":"9d9684533b01990b8d5d54aeaaa4474c","index":{"url":"z_755815454394ceaf_views_py.html","file":"comments\\views.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":59,"n_excluded":0,"n_missing":20,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"manage_py":{"hash":"a1683218582a5be3f4e12146fbc62422","index":{"url":"manage_py.html","file":"manage.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_c67bde34194b83d1___init___py":{"hash":"3c77fc9ef7f887ac2508d4109cf92472","index":{"url":"z_c67bde34194b83d1___init___py.html","file":"secfit\\__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_c67bde34194b83d1_asgi_py":{"hash":"619ebf8c97f382fc16d1be3a06bf39ec","index":{"url":"z_c67bde34194b83d1_asgi_py.html","file":"secfit\\asgi.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":4,"n_excluded":0,"n_missing":4,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c67bde34194b83d1_settings_py":{"hash":"a2a9f77eaca0d19656e5edc34932b78b","index":{"url":"z_c67bde34194b83d1_settings_py.html","file":"secfit\\settings.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":30,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_c67bde34194b83d1_urls_py":{"hash":"ce09d68d748e1dee404d0eb401c9979a","index":{"url":"z_c67bde34194b83d1_urls_py.html","file":"secfit\\urls.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_c67bde34194b83d1_wsgi_py":{"hash":"59a4f135ded83fbd42b34193ad317a1c","index":{"url":"z_c67bde34194b83d1_wsgi_py.html","file":"secfit\\wsgi.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":4,"n_excluded":0,"n_missing":4,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_a44f0ac069e85531___init___py":{"hash":"c2af740038c802fccc1f530c4f04bb9e","index":{"url":"z_a44f0ac069e85531___init___py.html","file":"tests\\__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_a44f0ac069e85531_test_TC001_py":{"hash":"d9e8aa11513b109bf0c2e516747337ce","index":{"url":"z_a44f0ac069e85531_test_TC001_py.html","file":"tests\\test_TC001.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":26,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_a44f0ac069e85531_test_TC002_py":{"hash":"6762428ec4b58f2cddd8359e6663cd09","index":{"url":"z_a44f0ac069e85531_test_TC002_py.html","file":"tests\\test_TC002.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":43,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_a44f0ac069e85531_test_TC003_py":{"hash":"2243bfdc571faa1683cbcaae84a17f9c","index":{"url":"z_a44f0ac069e85531_test_TC003_py.html","file":"tests\\test_TC003.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":22,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_a44f0ac069e85531_test_TC004_py":{"hash":"b6cf24ac1ca550c394b9bb87899f2993","index":{"url":"z_a44f0ac069e85531_test_TC004_py.html","file":"tests\\test_TC004.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":25,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_a44f0ac069e85531_test_TC005_py":{"hash":"3b12ef688c91507d7fc918c3fcda01d3","index":{"url":"z_a44f0ac069e85531_test_TC005_py.html","file":"tests\\test_TC005.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":28,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f6c68bdc9becfc1e___init___py":{"hash":"3c77fc9ef7f887ac2508d4109cf92472","index":{"url":"z_f6c68bdc9becfc1e___init___py.html","file":"users\\__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_f6c68bdc9becfc1e_admin_py":{"hash":"01709a5d5655861a398c3692d1cc4f10","index":{"url":"z_f6c68bdc9becfc1e_admin_py.html","file":"users\\admin.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":14,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f6c68bdc9becfc1e_apps_py":{"hash":"c37753bb21cd9fca5805b81b83a160a2","index":{"url":"z_f6c68bdc9becfc1e_apps_py.html","file":"users\\apps.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":3,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f6c68bdc9becfc1e_auth_backend_py":{"hash":"d5d997f4ff6f2ed9712463bf17d11327","index":{"url":"z_f6c68bdc9becfc1e_auth_backend_py.html","file":"users\\auth_backend.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":15,"n_excluded":0,"n_missing":15,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f6c68bdc9becfc1e_forms_py":{"hash":"597c62af23a4ac5c1ff1db454e33773e","index":{"url":"z_f6c68bdc9becfc1e_forms_py.html","file":"users\\forms.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":11,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_fff39e3ca0aaeed4_0001_initial_py":{"hash":"1ba54fcd83e984883a5c1625378764bc","index":{"url":"z_fff39e3ca0aaeed4_0001_initial_py.html","file":"users\\migrations\\0001_initial.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":11,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_fff39e3ca0aaeed4_0002_user_iscoach_py":{"hash":"2e1273f0b200ff4f5c0fafb0c12893bf","index":{"url":"z_fff39e3ca0aaeed4_0002_user_iscoach_py.html","file":"users\\migrations\\0002_user_iscoach.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":4,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_fff39e3ca0aaeed4_0003_user_specialism_py":{"hash":"64c2f2532252fbe59277c8e249b68fc3","index":{"url":"z_fff39e3ca0aaeed4_0003_user_specialism_py.html","file":"users\\migrations\\0003_user_specialism.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":4,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_fff39e3ca0aaeed4___init___py":{"hash":"3c77fc9ef7f887ac2508d4109cf92472","index":{"url":"z_fff39e3ca0aaeed4___init___py.html","file":"users\\migrations\\__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_f6c68bdc9becfc1e_models_py":{"hash":"8c16be893500a727fcd24037fbce7137","index":{"url":"z_f6c68bdc9becfc1e_models_py.html","file":"users\\models.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":23,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f6c68bdc9becfc1e_permissions_py":{"hash":"a9bc562e2a544fd9f08ed6cf91a05a16","index":{"url":"z_f6c68bdc9becfc1e_permissions_py.html","file":"users\\permissions.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":36,"n_excluded":0,"n_missing":15,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f6c68bdc9becfc1e_serializers_py":{"hash":"3553de3c70868eb49886d94da69e758f","index":{"url":"z_f6c68bdc9becfc1e_serializers_py.html","file":"users\\serializers.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":57,"n_excluded":0,"n_missing":24,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f6c68bdc9becfc1e_urls_py":{"hash":"24e373c2378ac5ec403ac33a1c54ff98","index":{"url":"z_f6c68bdc9becfc1e_urls_py.html","file":"users\\urls.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":4,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f6c68bdc9becfc1e_validators_py":{"hash":"d3a51542629a830288db270e78f0b374","index":{"url":"z_f6c68bdc9becfc1e_validators_py.html","file":"users\\validators.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":43,"n_excluded":0,"n_missing":17,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_f6c68bdc9becfc1e_views_py":{"hash":"3a0207d7dd33806c8b351d9840d22d03","index":{"url":"z_f6c68bdc9becfc1e_views_py.html","file":"users\\views.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":145,"n_excluded":0,"n_missing":64,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_8c45c045706a6dc0___init___py":{"hash":"3c77fc9ef7f887ac2508d4109cf92472","index":{"url":"z_8c45c045706a6dc0___init___py.html","file":"workouts\\__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_8c45c045706a6dc0_admin_py":{"hash":"0391d9ff18f5a67380ffd3f58261dc3b","index":{"url":"z_8c45c045706a6dc0_admin_py.html","file":"workouts\\admin.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":6,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_8c45c045706a6dc0_apps_py":{"hash":"3d17e2117460b7a1ac0ab6a34beb94a9","index":{"url":"z_8c45c045706a6dc0_apps_py.html","file":"workouts\\apps.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":3,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_a5b840cb18d5964e_0001_initial_py":{"hash":"25ea14db559212b6fcd520cc21b7d318","index":{"url":"z_a5b840cb18d5964e_0001_initial_py.html","file":"workouts\\migrations\\0001_initial.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":8,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_a5b840cb18d5964e___init___py":{"hash":"3c77fc9ef7f887ac2508d4109cf92472","index":{"url":"z_a5b840cb18d5964e___init___py.html","file":"workouts\\migrations\\__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_8c45c045706a6dc0_mixins_py":{"hash":"19d1c531227698abd67705168aba293b","index":{"url":"z_8c45c045706a6dc0_mixins_py.html","file":"workouts\\mixins.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":5,"n_excluded":0,"n_missing":1,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_8c45c045706a6dc0_models_py":{"hash":"3c8e49af1abee5a3886135e713b54e6a","index":{"url":"z_8c45c045706a6dc0_models_py.html","file":"workouts\\models.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":40,"n_excluded":0,"n_missing":5,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_8c45c045706a6dc0_parsers_py":{"hash":"04870579623dff1ffa0cd424d1cc40b4","index":{"url":"z_8c45c045706a6dc0_parsers_py.html","file":"workouts\\parsers.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":21,"n_excluded":0,"n_missing":17,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_8c45c045706a6dc0_permissions_py":{"hash":"0ee81233e9bb081cdabfa195a8ce2475","index":{"url":"z_8c45c045706a6dc0_permissions_py.html","file":"workouts\\permissions.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":32,"n_excluded":0,"n_missing":15,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_8c45c045706a6dc0_serializers_py":{"hash":"0834d59f2a53367bccc066ddf98d9ef8","index":{"url":"z_8c45c045706a6dc0_serializers_py.html","file":"workouts\\serializers.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":75,"n_excluded":0,"n_missing":35,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_8c45c045706a6dc0_urls_py":{"hash":"53507aed1e025581a042b2c63010867e","index":{"url":"z_8c45c045706a6dc0_urls_py.html","file":"workouts\\urls.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":4,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_8c45c045706a6dc0_views_py":{"hash":"f83672a2ba36a31f856278ab719baea2","index":{"url":"z_8c45c045706a6dc0_views_py.html","file":"workouts\\views.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":114,"n_excluded":0,"n_missing":29,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}}}} \ No newline at end of file diff --git a/backend/htmlcov/style_cb_718ce007.css b/backend/htmlcov/style_cb_718ce007.css new file mode 100644 index 0000000..3cdaf05 --- /dev/null +++ b/backend/htmlcov/style_cb_718ce007.css @@ -0,0 +1,337 @@ +@charset "UTF-8"; +/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/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 { color: #0451a5; } + +@media (prefers-color-scheme: dark) { #source p .t .str { 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.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.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.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 .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; 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 th { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-style: italic; color: #333; cursor: pointer; } + +@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } + +#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 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-top: 1px solid #ccc; 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_66f0b8a779cd646b_0001_initial_py.html b/backend/htmlcov/z_66f0b8a779cd646b_0001_initial_py.html new file mode 100644 index 0000000..f6581b2 --- /dev/null +++ b/backend/htmlcov/z_66f0b8a779cd646b_0001_initial_py.html @@ -0,0 +1,137 @@ + + + + + Coverage for comments\migrations\0001_initial.py: 100% + + + + + +
+
+

+ Coverage for comments\migrations\0001_initial.py: + 100% +

+ +

+ 7 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1# Generated by Django 4.0.8 on 2024-08-15 08:05 

+

2 

+

3from django.conf import settings 

+

4from django.db import migrations, models 

+

5import django.db.models.deletion 

+

6 

+

7 

+

8class Migration(migrations.Migration): 

+

9 

+

10 initial = True 

+

11 

+

12 dependencies = [ 

+

13 ('workouts', '0001_initial'), 

+

14 migrations.swappable_dependency(settings.AUTH_USER_MODEL), 

+

15 ] 

+

16 

+

17 operations = [ 

+

18 migrations.CreateModel( 

+

19 name='Comment', 

+

20 fields=[ 

+

21 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 

+

22 ('content', models.TextField()), 

+

23 ('timestamp', models.DateTimeField(auto_now_add=True)), 

+

24 ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL)), 

+

25 ('workout', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='workouts.workout')), 

+

26 ], 

+

27 options={ 

+

28 'ordering': ['-timestamp'], 

+

29 }, 

+

30 ), 

+

31 migrations.CreateModel( 

+

32 name='Like', 

+

33 fields=[ 

+

34 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 

+

35 ('timestamp', models.DateTimeField(auto_now_add=True)), 

+

36 ('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to='comments.comment')), 

+

37 ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes', to=settings.AUTH_USER_MODEL)), 

+

38 ], 

+

39 ), 

+

40 ] 

+
+ + + diff --git a/backend/htmlcov/z_66f0b8a779cd646b___init___py.html b/backend/htmlcov/z_66f0b8a779cd646b___init___py.html new file mode 100644 index 0000000..d1fbe0d --- /dev/null +++ b/backend/htmlcov/z_66f0b8a779cd646b___init___py.html @@ -0,0 +1,97 @@ + + + + + Coverage for comments\migrations\__init__.py: 100% + + + + + +
+
+

+ Coverage for comments\migrations\__init__.py: + 100% +

+ +

+ 0 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+
+ + + diff --git a/backend/htmlcov/z_755815454394ceaf___init___py.html b/backend/htmlcov/z_755815454394ceaf___init___py.html new file mode 100644 index 0000000..7464794 --- /dev/null +++ b/backend/htmlcov/z_755815454394ceaf___init___py.html @@ -0,0 +1,97 @@ + + + + + Coverage for comments\__init__.py: 100% + + + + + +
+
+

+ Coverage for comments\__init__.py: + 100% +

+ +

+ 0 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+
+ + + diff --git a/backend/htmlcov/z_755815454394ceaf_admin_py.html b/backend/htmlcov/z_755815454394ceaf_admin_py.html new file mode 100644 index 0000000..2171558 --- /dev/null +++ b/backend/htmlcov/z_755815454394ceaf_admin_py.html @@ -0,0 +1,102 @@ + + + + + Coverage for comments\admin.py: 100% + + + + + +
+
+

+ Coverage for comments\admin.py: + 100% +

+ +

+ 3 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from django.contrib import admin 

+

2 

+

3from .models import Comment 

+

4 

+

5admin.site.register(Comment) 

+
+ + + diff --git a/backend/htmlcov/z_755815454394ceaf_apps_py.html b/backend/htmlcov/z_755815454394ceaf_apps_py.html new file mode 100644 index 0000000..d3c2590 --- /dev/null +++ b/backend/htmlcov/z_755815454394ceaf_apps_py.html @@ -0,0 +1,102 @@ + + + + + Coverage for comments\apps.py: 100% + + + + + +
+
+

+ Coverage for comments\apps.py: + 100% +

+ +

+ 3 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from django.apps import AppConfig 

+

2 

+

3 

+

4class CommentsConfig(AppConfig): 

+

5 name = "comments" 

+
+ + + diff --git a/backend/htmlcov/z_755815454394ceaf_models_py.html b/backend/htmlcov/z_755815454394ceaf_models_py.html new file mode 100644 index 0000000..a58a753 --- /dev/null +++ b/backend/htmlcov/z_755815454394ceaf_models_py.html @@ -0,0 +1,144 @@ + + + + + Coverage for comments\models.py: 100% + + + + + +
+
+

+ Coverage for comments\models.py: + 100% +

+ +

+ 19 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from django.db import models 

+

2 

+

3 

+

4from django.conf import settings 

+

5from django.contrib.contenttypes.fields import GenericForeignKey 

+

6from django.contrib.contenttypes.models import ContentType 

+

7from django.urls import reverse 

+

8from django.db import models 

+

9from django.contrib.auth import get_user_model 

+

10from workouts.models import Workout 

+

11 

+

12class Comment(models.Model): 

+

13 """Django model for a comment left on a workout. 

+

14 

+

15 Attributes: 

+

16 owner: Who posted the comment 

+

17 workout: The workout this comment was left on. 

+

18 content: The content of the comment. 

+

19 timestamp: When the comment was created. 

+

20 """ 

+

21 owner = models.ForeignKey( 

+

22 get_user_model(), on_delete=models.CASCADE, related_name="comments" 

+

23 ) 

+

24 workout = models.ForeignKey( 

+

25 Workout, on_delete=models.CASCADE, related_name="comments" 

+

26 ) 

+

27 content = models.TextField() 

+

28 timestamp = models.DateTimeField(auto_now_add=True) 

+

29 

+

30 class Meta: 

+

31 ordering = ["-timestamp"] 

+

32 

+

33 

+

34class Like(models.Model): 

+

35 """Django model for a reaction to a comment. 

+

36 

+

37 

+

38 Attributes: 

+

39 owner: Who liked the comment 

+

40 comment: The comment that was liked 

+

41 timestamp: When the like occurred. 

+

42 """ 

+

43 owner = models.ForeignKey( 

+

44 get_user_model(), on_delete=models.CASCADE, related_name="likes" 

+

45 ) 

+

46 comment = models.ForeignKey(Comment, on_delete=models.CASCADE, related_name="likes") 

+

47 timestamp = models.DateTimeField(auto_now_add=True) 

+
+ + + diff --git a/backend/htmlcov/z_755815454394ceaf_permissions_py.html b/backend/htmlcov/z_755815454394ceaf_permissions_py.html new file mode 100644 index 0000000..340bee8 --- /dev/null +++ b/backend/htmlcov/z_755815454394ceaf_permissions_py.html @@ -0,0 +1,118 @@ + + + + + Coverage for comments\permissions.py: 75% + + + + + +
+
+

+ Coverage for comments\permissions.py: + 75% +

+ +

+ 4 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from rest_framework import permissions 

+

2 

+

3 

+

4class IsCommentVisibleToUser(permissions.BasePermission): 

+

5 """ 

+

6 Custom permission to only allow a comment to be viewed 

+

7 if one of the following holds: 

+

8 - The comment is on a public visibility workout 

+

9 - The comment was written by the user 

+

10 - The comment is on a coach visibility workout and the user is the workout owner's coach 

+

11 - The comment is on a workout owned by the user 

+

12 """ 

+

13 

+

14 def has_object_permission(self, request, view, obj): 

+

15 # Write permissions are only allowed to the owner. 

+

16 return ( 

+

17 obj.workout.visibility == "PU" 

+

18 or obj.owner == request.user 

+

19 or (obj.workout.visibility == "CO" and obj.owner.coach == request.user) 

+

20 or obj.workout.owner == request.user 

+

21 ) 

+
+ + + diff --git a/backend/htmlcov/z_755815454394ceaf_serializers_py.html b/backend/htmlcov/z_755815454394ceaf_serializers_py.html new file mode 100644 index 0000000..5139a7b --- /dev/null +++ b/backend/htmlcov/z_755815454394ceaf_serializers_py.html @@ -0,0 +1,123 @@ + + + + + Coverage for comments\serializers.py: 100% + + + + + +
+
+

+ Coverage for comments\serializers.py: + 100% +

+ +

+ 16 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from rest_framework import serializers 

+

2from rest_framework.serializers import HyperlinkedRelatedField 

+

3from .models import Comment, Like 

+

4from workouts.models import Workout 

+

5 

+

6 

+

7class CommentSerializer(serializers.HyperlinkedModelSerializer): 

+

8 owner = serializers.ReadOnlyField(source="owner.username") 

+

9 workout = HyperlinkedRelatedField( 

+

10 queryset=Workout.objects.all(), view_name="workout-detail" 

+

11 ) 

+

12 

+

13 class Meta: 

+

14 model = Comment 

+

15 fields = ["url", "id", "owner", "workout", "content", "timestamp"] 

+

16 

+

17 

+

18class LikeSerializer(serializers.HyperlinkedModelSerializer): 

+

19 owner = serializers.ReadOnlyField(source="owner.username") 

+

20 comment = HyperlinkedRelatedField( 

+

21 queryset=Comment.objects.all(), view_name="comment-detail" 

+

22 ) 

+

23 

+

24 class Meta: 

+

25 model = Like 

+

26 fields = ["url", "id", "owner", "comment", "timestamp"] 

+
+ + + diff --git a/backend/htmlcov/z_755815454394ceaf_urls_py.html b/backend/htmlcov/z_755815454394ceaf_urls_py.html new file mode 100644 index 0000000..997b8f8 --- /dev/null +++ b/backend/htmlcov/z_755815454394ceaf_urls_py.html @@ -0,0 +1,108 @@ + + + + + Coverage for comments\urls.py: 100% + + + + + +
+
+

+ Coverage for comments\urls.py: + 100% +

+ +

+ 5 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from django.urls import path, include 

+

2from .models import Comment, Like 

+

3from .views import CommentList, CommentDetail, LikeList, LikeDetail 

+

4from rest_framework.urlpatterns import format_suffix_patterns 

+

5 

+

6urlpatterns = [ 

+

7 path("api/comments/", CommentList.as_view(), name="comment-list"), 

+

8 path("api/comments/<int:pk>/", CommentDetail.as_view(), name="comment-detail"), 

+

9 path("api/likes/", LikeList.as_view(), name="like-list"), 

+

10 path("api/likes/<int:pk>/", LikeDetail.as_view(), name="like-detail"), 

+

11] 

+
+ + + diff --git a/backend/htmlcov/z_755815454394ceaf_views_py.html b/backend/htmlcov/z_755815454394ceaf_views_py.html new file mode 100644 index 0000000..f64e4a2 --- /dev/null +++ b/backend/htmlcov/z_755815454394ceaf_views_py.html @@ -0,0 +1,207 @@ + + + + + Coverage for comments\views.py: 66% + + + + + +
+
+

+ Coverage for comments\views.py: + 66% +

+ +

+ 59 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from django.shortcuts import render 

+

2from rest_framework import generics, mixins 

+

3from .models import Comment, Like 

+

4from rest_framework import permissions 

+

5from .permissions import IsCommentVisibleToUser 

+

6from workouts.permissions import IsOwner, IsReadOnly 

+

7from .serializers import CommentSerializer, LikeSerializer 

+

8from django.db.models import Q 

+

9from rest_framework.filters import OrderingFilter 

+

10 

+

11class CommentList( 

+

12 mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView 

+

13): 

+

14 serializer_class = CommentSerializer 

+

15 permission_classes = [permissions.IsAuthenticated] 

+

16 filter_backends = [OrderingFilter] 

+

17 ordering_fields = ["timestamp"] 

+

18 

+

19 def get(self, request, *args, **kwargs): 

+

20 return self.list(request, *args, **kwargs) 

+

21 

+

22 def post(self, request, *args, **kwargs): 

+

23 return self.create(request, *args, **kwargs) 

+

24 

+

25 def perform_create(self, serializer): 

+

26 serializer.save(owner=self.request.user) 

+

27 

+

28 def get_queryset(self): 

+

29 workout_pk = self.kwargs.get("pk") 

+

30 qs = Comment.objects.none() 

+

31 

+

32 if workout_pk: 

+

33 qs = Comment.objects.filter(workout=workout_pk) 

+

34 elif self.request.user: 

+

35 """A comment should be visible to the requesting user if any of the following hold: 

+

36 - The comment is on a public visibility workout 

+

37 - The comment was written by the user 

+

38 - The comment is on a coach visibility workout and the user is the workout owner's coach 

+

39 - The comment is on a workout owned by the user 

+

40 """ 

+

41 qs = Comment.objects.filter( 

+

42 Q(workout__visibility="PU") 

+

43 | Q(owner=self.request.user) 

+

44 | ( 

+

45 Q(workout__visibility="CO") 

+

46 & Q(workout__owner__coach=self.request.user) 

+

47 ) 

+

48 | Q(workout__owner=self.request.user) 

+

49 ).distinct() 

+

50 

+

51 return qs 

+

52 

+

53 

+

54class CommentDetail( 

+

55 mixins.RetrieveModelMixin, 

+

56 mixins.UpdateModelMixin, 

+

57 mixins.DestroyModelMixin, 

+

58 generics.GenericAPIView, 

+

59): 

+

60 queryset = Comment.objects.all() 

+

61 serializer_class = CommentSerializer 

+

62 permission_classes = [ 

+

63 permissions.IsAuthenticated & IsCommentVisibleToUser & (IsOwner | IsReadOnly) 

+

64 ] 

+

65 

+

66 def get(self, request, *args, **kwargs): 

+

67 return self.retrieve(request, *args, **kwargs) 

+

68 

+

69 def put(self, request, *args, **kwargs): 

+

70 return self.update(request, *args, **kwargs) 

+

71 

+

72 def delete(self, request, *args, **kwargs): 

+

73 return self.destroy(request, *args, **kwargs) 

+

74 

+

75 

+

76class LikeList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView): 

+

77 serializer_class = LikeSerializer 

+

78 permission_classes = [permissions.IsAuthenticated] 

+

79 

+

80 def get(self, request, *args, **kwargs): 

+

81 return self.list(request, *args, **kwargs) 

+

82 

+

83 def post(self, request, *args, **kwargs): 

+

84 return self.create(request, *args, **kwargs) 

+

85 

+

86 def perform_create(self, serializer): 

+

87 serializer.save(owner=self.request.user) 

+

88 

+

89 def get_queryset(self): 

+

90 return Like.objects.filter(owner=self.request.user) 

+

91 

+

92 

+

93class LikeDetail( 

+

94 mixins.RetrieveModelMixin, 

+

95 mixins.UpdateModelMixin, 

+

96 mixins.DestroyModelMixin, 

+

97 generics.GenericAPIView, 

+

98): 

+

99 queryset = Like.objects.all() 

+

100 serializer_class = LikeSerializer 

+

101 permission_classes = [permissions.IsAuthenticated] 

+

102 

+

103 def get(self, request, *args, **kwargs): 

+

104 return self.retrieve(request, *args, **kwargs) 

+

105 

+

106 def put(self, request, *args, **kwargs): 

+

107 return self.update(request, *args, **kwargs) 

+

108 

+

109 def delete(self, request, *args, **kwargs): 

+

110 return self.destroy(request, *args, **kwargs) 

+
+ + + diff --git a/backend/htmlcov/z_8c45c045706a6dc0___init___py.html b/backend/htmlcov/z_8c45c045706a6dc0___init___py.html new file mode 100644 index 0000000..554859e --- /dev/null +++ b/backend/htmlcov/z_8c45c045706a6dc0___init___py.html @@ -0,0 +1,97 @@ + + + + + Coverage for workouts\__init__.py: 100% + + + + + +
+
+

+ Coverage for workouts\__init__.py: + 100% +

+ +

+ 0 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+
+ + + diff --git a/backend/htmlcov/z_8c45c045706a6dc0_admin_py.html b/backend/htmlcov/z_8c45c045706a6dc0_admin_py.html new file mode 100644 index 0000000..a33802b --- /dev/null +++ b/backend/htmlcov/z_8c45c045706a6dc0_admin_py.html @@ -0,0 +1,107 @@ + + + + + Coverage for workouts\admin.py: 100% + + + + + +
+
+

+ Coverage for workouts\admin.py: + 100% +

+ +

+ 6 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1"""Module for registering models from workouts app to admin page so that they appear 

+

2""" 

+

3from django.contrib import admin 

+

4 

+

5from .models import Exercise, ExerciseInstance, Workout, WorkoutFile 

+

6 

+

7admin.site.register(Exercise) 

+

8admin.site.register(ExerciseInstance) 

+

9admin.site.register(Workout) 

+

10admin.site.register(WorkoutFile) 

+
+ + + diff --git a/backend/htmlcov/z_8c45c045706a6dc0_apps_py.html b/backend/htmlcov/z_8c45c045706a6dc0_apps_py.html new file mode 100644 index 0000000..dde6788 --- /dev/null +++ b/backend/htmlcov/z_8c45c045706a6dc0_apps_py.html @@ -0,0 +1,109 @@ + + + + + Coverage for workouts\apps.py: 100% + + + + + +
+
+

+ Coverage for workouts\apps.py: + 100% +

+ +

+ 3 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1"""AppConfig for workouts app 

+

2""" 

+

3from django.apps import AppConfig 

+

4 

+

5class WorkoutsConfig(AppConfig): 

+

6 """AppConfig for workouts app 

+

7 

+

8 Attributes: 

+

9 name (str): The name of the application 

+

10 """ 

+

11 

+

12 name = "workouts" 

+
+ + + diff --git a/backend/htmlcov/z_8c45c045706a6dc0_mixins_py.html b/backend/htmlcov/z_8c45c045706a6dc0_mixins_py.html new file mode 100644 index 0000000..b93322d --- /dev/null +++ b/backend/htmlcov/z_8c45c045706a6dc0_mixins_py.html @@ -0,0 +1,122 @@ + + + + + Coverage for workouts\mixins.py: 80% + + + + + +
+
+

+ Coverage for workouts\mixins.py: + 80% +

+ +

+ 5 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1""" 

+

2Mixins for the workouts application 

+

3""" 

+

4 

+

5class CreateListModelMixin(object): 

+

6 """Mixin that allows to create multiple objects from lists. 

+

7 Taken from https://stackoverflow.com/a/48885641 

+

8 """ 

+

9 

+

10 def get_serializer(self, *args, **kwargs): 

+

11 """If an array is passed, set serializer to many. 

+

12 

+

13 kwargs["many"] will be set to true if an array is passed. This argument 

+

14 is passed when retrieving the serializer. 

+

15 

+

16 Args: 

+

17 *args: Variable length argument list passed to the serializer. 

+

18 **kwargs: Arbitrary keyword arguments passed to the serializer, including "many". 

+

19 

+

20 Returns: 

+

21 [type]: [description] 

+

22 """ 

+

23 if isinstance(kwargs.get("data", {}), list): 

+

24 kwargs["many"] = True 

+

25 return super(CreateListModelMixin, self).get_serializer(*args, **kwargs) 

+
+ + + diff --git a/backend/htmlcov/z_8c45c045706a6dc0_models_py.html b/backend/htmlcov/z_8c45c045706a6dc0_models_py.html new file mode 100644 index 0000000..25475bf --- /dev/null +++ b/backend/htmlcov/z_8c45c045706a6dc0_models_py.html @@ -0,0 +1,237 @@ + + + + + Coverage for workouts\models.py: 88% + + + + + +
+
+

+ Coverage for workouts\models.py: + 88% +

+ +

+ 40 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1"""Contains the models for the workouts Django application. Users 

+

2log workouts (Workout), which contain instances (ExerciseInstance) of various 

+

3type of exercises (Exercise). The user can also upload files (WorkoutFile) . 

+

4""" 

+

5import os 

+

6from django.db import models 

+

7from django.core.files.storage import FileSystemStorage 

+

8from django.conf import settings 

+

9from django.contrib.auth import get_user_model 

+

10 

+

11 

+

12class OverwriteStorage(FileSystemStorage): 

+

13 """Filesystem storage for overwriting files. Currently unused.""" 

+

14 

+

15 def get_available_name(self, name, max_length=None): 

+

16 """https://djangosnippets.org/snippets/976/ 

+

17 Returns a filename that's free on the target storage system, and 

+

18 available for new content to be written to. 

+

19 

+

20 Args: 

+

21 name (str): Name of the file 

+

22 max_length (int, optional): Maximum length of a file name. Defaults to None. 

+

23 """ 

+

24 if self.exists(name): 

+

25 os.remove(os.path.join(settings.MEDIA_ROOT, name)) 

+

26 

+

27 

+

28class Workout(models.Model): 

+

29 """Django model for a workout that users can log. 

+

30 

+

31 A workout has several attributes, and is associated with one or more exercises 

+

32 (instances) and, optionally, files uploaded by the user. 

+

33 

+

34 Attributes: 

+

35 name: Name of the workout 

+

36 date: Date the workout was performed or is planned 

+

37 notes: Notes about the workout 

+

38 owner: User that logged the workout 

+

39 visibility: The visibility level of the workout: Public, Coach, or Private 

+

40 """ 

+

41 

+

42 name = models.CharField(max_length=100) 

+

43 date = models.DateTimeField() 

+

44 notes = models.TextField() 

+

45 owner = models.ForeignKey( 

+

46 get_user_model(), on_delete=models.CASCADE, related_name="workouts" 

+

47 ) 

+

48 

+

49 # Visibility levels 

+

50 PUBLIC = "PU" # Visible to all authenticated users 

+

51 COACH = "CO" # Visible only to owner and their coach 

+

52 PRIVATE = "PR" # Visible only to owner 

+

53 VISIBILITY_CHOICES = [ 

+

54 (PUBLIC, "Public"), 

+

55 (COACH, "Coach"), 

+

56 (PRIVATE, "Private"), 

+

57 ] # Choices for visibility level 

+

58 

+

59 visibility = models.CharField( 

+

60 max_length=2, choices=VISIBILITY_CHOICES, default=COACH 

+

61 ) 

+

62 

+

63 class Meta: 

+

64 ordering = ["-date"] 

+

65 

+

66 def __str__(self): 

+

67 return self.name 

+

68 

+

69 

+

70class Exercise(models.Model): 

+

71 """Django model for an exercise type that users can create. 

+

72 

+

73 Each exercise instance must have an exercise type, e.g., Pushups, Crunches, or Lunges. 

+

74 

+

75 Attributes: 

+

76 name: Name of the exercise type 

+

77 description: Description of the exercise type 

+

78 unit: Name of the unit for the exercise type (e.g., reps, seconds) 

+

79 """ 

+

80 

+

81 name = models.CharField(max_length=100) 

+

82 description = models.TextField() 

+

83 unit = models.CharField(max_length=50) 

+

84 

+

85 def __str__(self): 

+

86 return self.name 

+

87 

+

88 

+

89class ExerciseInstance(models.Model): 

+

90 """Django model for an instance of an exercise. 

+

91 

+

92 Each workout has one or more exercise instances, each of a given type. For example, 

+

93 Kyle's workout on 15.06.2029 had one exercise instance: 3 (sets) reps (unit) of 

+

94 10 (number) pushups (exercise type) 

+

95 

+

96 Attributes: 

+

97 workout: The workout associated with this exercise instance 

+

98 exercise: The exercise type of this instance 

+

99 sets: The number of sets the owner will perform/performed 

+

100 number: The number of repetitions in each set the owner will perform/performed 

+

101 """ 

+

102 

+

103 workout = models.ForeignKey( 

+

104 Workout, on_delete=models.CASCADE, related_name="exercise_instances" 

+

105 ) 

+

106 exercise = models.ForeignKey( 

+

107 Exercise, on_delete=models.CASCADE, related_name="instances" 

+

108 ) 

+

109 sets = models.IntegerField() 

+

110 number = models.IntegerField() 

+

111 

+

112 

+

113def workout_directory_path(instance, filename): 

+

114 """Return path for which workout files should be uploaded on the web server 

+

115 

+

116 Args: 

+

117 instance (WorkoutFile): WorkoutFile instance 

+

118 filename (str): Name of the file 

+

119 

+

120 Returns: 

+

121 str: Path where workout file is stored 

+

122 """ 

+

123 return f"workouts/{instance.workout.id}/{filename}" 

+

124 

+

125 

+

126class WorkoutFile(models.Model): 

+

127 """Django model for file associated with a workout. Basically a wrapper. 

+

128 

+

129 Attributes: 

+

130 workout: The workout for which this file has been uploaded 

+

131 owner: The user who uploaded the file 

+

132 file: The actual file that's being uploaded 

+

133 """ 

+

134 

+

135 workout = models.ForeignKey(Workout, on_delete=models.CASCADE, related_name="files") 

+

136 owner = models.ForeignKey( 

+

137 get_user_model(), on_delete=models.CASCADE, related_name="workout_files" 

+

138 ) 

+

139 file = models.FileField(upload_to=workout_directory_path) 

+

140 

+
+ + + diff --git a/backend/htmlcov/z_8c45c045706a6dc0_parsers_py.html b/backend/htmlcov/z_8c45c045706a6dc0_parsers_py.html new file mode 100644 index 0000000..5638c90 --- /dev/null +++ b/backend/htmlcov/z_8c45c045706a6dc0_parsers_py.html @@ -0,0 +1,135 @@ + + + + + Coverage for workouts\parsers.py: 19% + + + + + +
+
+

+ Coverage for workouts\parsers.py: + 19% +

+ +

+ 21 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1"""Contains custom parsers for serializers from the workouts Django app 

+

2""" 

+

3import json 

+

4from rest_framework import parsers 

+

5 

+

6# Thanks to https://stackoverflow.com/a/50514630 

+

7class MultipartJsonParser(parsers.MultiPartParser): 

+

8 """Parser for serializing multipart data containing both files and JSON. 

+

9 

+

10 This is currently unused. 

+

11 """ 

+

12 

+

13 def parse(self, stream, media_type=None, parser_context=None): 

+

14 result = super().parse( 

+

15 stream, media_type=media_type, parser_context=parser_context 

+

16 ) 

+

17 data = {} 

+

18 new_files = {"files": []} 

+

19 

+

20 # for case1 with nested serializers 

+

21 # parse each field with json 

+

22 for key, value in result.data.items(): 

+

23 if not isinstance(value, str): 

+

24 data[key] = value 

+

25 continue 

+

26 if "{" in value or "[" in value: 

+

27 try: 

+

28 data[key] = json.loads(value) 

+

29 except ValueError: 

+

30 data[key] = value 

+

31 else: 

+

32 data[key] = value 

+

33 

+

34 files = result.files.getlist("files") 

+

35 for file in files: 

+

36 new_files["files"].append({"file": file}) 

+

37 

+

38 return parsers.DataAndFiles(data, new_files) 

+
+ + + diff --git a/backend/htmlcov/z_8c45c045706a6dc0_permissions_py.html b/backend/htmlcov/z_8c45c045706a6dc0_permissions_py.html new file mode 100644 index 0000000..aa6a63a --- /dev/null +++ b/backend/htmlcov/z_8c45c045706a6dc0_permissions_py.html @@ -0,0 +1,169 @@ + + + + + Coverage for workouts\permissions.py: 53% + + + + + +
+
+

+ Coverage for workouts\permissions.py: + 53% +

+ +

+ 32 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1"""Contains custom DRF permissions classes for the workouts app 

+

2""" 

+

3from rest_framework import permissions 

+

4from workouts.models import Workout 

+

5 

+

6 

+

7class IsOwner(permissions.BasePermission): 

+

8 """Checks whether the requesting user is also the owner of the existing object""" 

+

9 

+

10 def has_object_permission(self, request, view, obj): 

+

11 return obj.owner == request.user 

+

12 

+

13 

+

14class IsOwnerOfWorkout(permissions.BasePermission): 

+

15 """Checks whether the requesting user is also the owner of the new or existing object""" 

+

16 

+

17 def has_permission(self, request, view): 

+

18 if request.method == "POST": 

+

19 if request.data.get("workout"): 

+

20 workout_id = request.data["workout"].split("/")[-2] 

+

21 workout = Workout.objects.get(pk=workout_id) 

+

22 if workout: 

+

23 return workout.owner == request.user 

+

24 return False 

+

25 

+

26 return True 

+

27 

+

28 def has_object_permission(self, request, view, obj): 

+

29 return obj.workout.owner == request.user 

+

30 

+

31 

+

32class IsCoachAndVisibleToCoach(permissions.BasePermission): 

+

33 """Checks whether the requesting user is the existing object's owner's coach 

+

34 and whether the object (workout) has a visibility of Public or Coach. 

+

35 """ 

+

36 

+

37 def has_object_permission(self, request, view, obj): 

+

38 return obj.owner.coach == request.user and ( 

+

39 obj.visibility == "PU" or obj.visibility == "CO" 

+

40 ) 

+

41 

+

42 

+

43class IsCoachOfWorkoutAndVisibleToCoach(permissions.BasePermission): 

+

44 """Checks whether the requesting user is the existing workout's owner's coach 

+

45 and whether the object has a visibility of Public or Coach. 

+

46 """ 

+

47 

+

48 def has_object_permission(self, request, view, obj): 

+

49 return obj.workout.owner.coach == request.user and ( 

+

50 obj.workout.visibility == "PU" or obj.workout.visibility == "CO" 

+

51 ) 

+

52 

+

53 

+

54class IsPublic(permissions.BasePermission): 

+

55 """Checks whether the object (workout) has visibility of Public.""" 

+

56 

+

57 def has_object_permission(self, request, view, obj): 

+

58 return obj.visibility == "PU" 

+

59 

+

60 

+

61class IsWorkoutPublic(permissions.BasePermission): 

+

62 """Checks whether the object's workout has visibility of Public.""" 

+

63 

+

64 def has_object_permission(self, request, view, obj): 

+

65 return obj.workout.visibility == "PU" 

+

66 

+

67 

+

68class IsReadOnly(permissions.BasePermission): 

+

69 """Checks whether the HTTP request verb is only for retrieving data (GET, HEAD, OPTIONS)""" 

+

70 

+

71 def has_object_permission(self, request, view, obj): 

+

72 return request.method in permissions.SAFE_METHODS 

+
+ + + diff --git a/backend/htmlcov/z_8c45c045706a6dc0_serializers_py.html b/backend/htmlcov/z_8c45c045706a6dc0_serializers_py.html new file mode 100644 index 0000000..db9bc78 --- /dev/null +++ b/backend/htmlcov/z_8c45c045706a6dc0_serializers_py.html @@ -0,0 +1,310 @@ + + + + + Coverage for workouts\serializers.py: 53% + + + + + +
+
+

+ Coverage for workouts\serializers.py: + 53% +

+ +

+ 75 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1"""Serializers for the workouts application 

+

2""" 

+

3from rest_framework import serializers 

+

4from rest_framework.serializers import HyperlinkedRelatedField 

+

5from .models import Workout, Exercise, ExerciseInstance, WorkoutFile 

+

6 

+

7 

+

8class ExerciseInstanceSerializer(serializers.HyperlinkedModelSerializer): 

+

9 """Serializer for an ExerciseInstance. Hyperlinks are used for relationships by default. 

+

10 

+

11 Serialized fields: url, id, exercise, sets, number, workout 

+

12 

+

13 Attributes: 

+

14 workout: The associated workout for this instance, represented by a hyperlink 

+

15 """ 

+

16 

+

17 workout = HyperlinkedRelatedField( 

+

18 queryset=Workout.objects.all(), view_name="workout-detail", required=False 

+

19 ) 

+

20 

+

21 class Meta: 

+

22 model = ExerciseInstance 

+

23 fields = ["url", "id", "exercise", "sets", "number", "workout"] 

+

24 

+

25 

+

26class WorkoutFileSerializer(serializers.HyperlinkedModelSerializer): 

+

27 """Serializer for a WorkoutFile. Hyperlinks are used for relationships by default. 

+

28 

+

29 Serialized fields: url, id, owner, file, workout 

+

30 

+

31 Attributes: 

+

32 owner: The owner (User) of the WorkoutFile, represented by a username. ReadOnly 

+

33 workout: The associate workout for this WorkoutFile, represented by a hyperlink 

+

34 """ 

+

35 

+

36 owner = serializers.ReadOnlyField(source="owner.username") 

+

37 workout = HyperlinkedRelatedField( 

+

38 queryset=Workout.objects.all(), view_name="workout-detail", required=False 

+

39 ) 

+

40 

+

41 class Meta: 

+

42 model = WorkoutFile 

+

43 fields = ["url", "id", "owner", "file", "workout"] 

+

44 

+

45 def create(self, validated_data): 

+

46 return WorkoutFile.objects.create(**validated_data) 

+

47 

+

48 

+

49class WorkoutSerializer(serializers.HyperlinkedModelSerializer): 

+

50 """Serializer for a Workout. Hyperlinks are used for relationships by default. 

+

51 

+

52 This serializer specifies nested serialization since a workout consists of WorkoutFiles 

+

53 and ExerciseInstances. 

+

54 

+

55 Serialized fields: url, id, name, date, notes, owner, owner_username, visiblity, 

+

56 exercise_instances, files 

+

57 

+

58 Attributes: 

+

59 owner_username: Username of the owning User 

+

60 exercise_instance: Serializer for ExericseInstances 

+

61 files: Serializer for WorkoutFiles 

+

62 """ 

+

63 

+

64 owner_username = serializers.SerializerMethodField() 

+

65 exercise_instances = ExerciseInstanceSerializer(many=True, required=True) 

+

66 files = WorkoutFileSerializer(many=True, required=False) 

+

67 

+

68 class Meta: 

+

69 model = Workout 

+

70 fields = [ 

+

71 "url", 

+

72 "id", 

+

73 "name", 

+

74 "date", 

+

75 "notes", 

+

76 "owner", 

+

77 "owner_username", 

+

78 "visibility", 

+

79 "exercise_instances", 

+

80 "files", 

+

81 ] 

+

82 extra_kwargs = {"owner": {"read_only": True}} 

+

83 

+

84 def create(self, validated_data): 

+

85 """Custom logic for creating ExerciseInstances, WorkoutFiles, and a Workout. 

+

86 

+

87 This is needed to iterate over the files and exercise instances, since this serializer is 

+

88 nested. 

+

89 

+

90 Args: 

+

91 validated_data: Validated files and exercise_instances 

+

92 

+

93 Returns: 

+

94 Workout: A newly created Workout 

+

95 """ 

+

96 exercise_instances_data = validated_data.pop("exercise_instances") 

+

97 files_data = [] 

+

98 if "files" in validated_data: 

+

99 files_data = validated_data.pop("files") 

+

100 

+

101 workout = Workout.objects.create(**validated_data) 

+

102 

+

103 for exercise_instance_data in exercise_instances_data: 

+

104 ExerciseInstance.objects.create(workout=workout, **exercise_instance_data) 

+

105 for file_data in files_data: 

+

106 WorkoutFile.objects.create( 

+

107 workout=workout, owner=workout.owner, file=file_data.get("file") 

+

108 ) 

+

109 

+

110 return workout 

+

111 

+

112 def update(self, instance, validated_data): 

+

113 """Custom logic for updating a Workout with its ExerciseInstances and Workouts. 

+

114 

+

115 Args: 

+

116 instance (Workout): Current Workout object 

+

117 validated_data: Contains data for validated fields 

+

118 

+

119 Returns: 

+

120 Workout: Updated Workout instance 

+

121 """ 

+

122 exercise_instances_data = validated_data.pop("exercise_instances", []) 

+

123 exercise_instances = list(instance.exercise_instances.all()) # Convert to list for consistent indexing 

+

124 

+

125 instance.name = validated_data.get("name", instance.name) 

+

126 instance.notes = validated_data.get("notes", instance.notes) 

+

127 instance.visibility = validated_data.get("visibility", instance.visibility) 

+

128 instance.date = validated_data.get("date", instance.date) 

+

129 instance.save() 

+

130 

+

131 # Handle ExerciseInstances 

+

132 for exercise_instance, exercise_instance_data in zip( 

+

133 exercise_instances, exercise_instances_data 

+

134 ): 

+

135 exercise_instance.exercise = exercise_instance_data.get( 

+

136 "exercise", exercise_instance.exercise 

+

137 ) 

+

138 exercise_instance.number = exercise_instance_data.get( 

+

139 "number", exercise_instance.number 

+

140 ) 

+

141 exercise_instance.sets = exercise_instance_data.get( 

+

142 "sets", exercise_instance.sets 

+

143 ) 

+

144 exercise_instance.save() 

+

145 

+

146 # If new exercise instances have been added, create them 

+

147 if len(exercise_instances_data) > len(exercise_instances): 

+

148 for i in range(len(exercise_instances), len(exercise_instances_data)): 

+

149 exercise_instance_data = exercise_instances_data[i] 

+

150 ExerciseInstance.objects.create( 

+

151 workout=instance, **exercise_instance_data 

+

152 ) 

+

153 

+

154 # If exercise instances have been removed, delete the extras 

+

155 elif len(exercise_instances_data) < len(exercise_instances): 

+

156 for i in range(len(exercise_instances_data), len(exercise_instances)): 

+

157 exercise_instances[i].delete() 

+

158 

+

159 # Handle WorkoutFiles 

+

160 if "files" in validated_data: 

+

161 files_data = validated_data.pop("files") 

+

162 files = list(instance.files.all()) # Convert to list for consistent indexing 

+

163 

+

164 for file, file_data in zip(files, files_data): 

+

165 file.file = file_data.get("file", file.file) 

+

166 file.save() 

+

167 

+

168 # If new files have been added, create new WorkoutFiles 

+

169 if len(files_data) > len(files): 

+

170 for i in range(len(files), len(files_data)): 

+

171 WorkoutFile.objects.create( 

+

172 workout=instance, 

+

173 owner=instance.owner, 

+

174 file=files_data[i].get("file"), 

+

175 ) 

+

176 

+

177 # If files have been removed, delete the extras 

+

178 elif len(files_data) < len(files): 

+

179 for i in range(len(files_data), len(files)): 

+

180 files[i].delete() 

+

181 

+

182 return instance 

+

183 

+

184 

+

185 def get_owner_username(self, obj): 

+

186 """Returns the owning user's username 

+

187 

+

188 Args: 

+

189 obj (Workout): Current Workout 

+

190 

+

191 Returns: 

+

192 str: Username of owner 

+

193 """ 

+

194 return obj.owner.username 

+

195 

+

196 

+

197class ExerciseSerializer(serializers.HyperlinkedModelSerializer): 

+

198 """Serializer for an Exercise. Hyperlinks are used for relationships by default. 

+

199 

+

200 Serialized fields: url, id, name, description, unit, instances 

+

201 

+

202 Attributes: 

+

203 instances: Associated exercise instances with this Exercise type. Hyperlinks. 

+

204 """ 

+

205 

+

206 instances = serializers.HyperlinkedRelatedField( 

+

207 many=True, view_name="exerciseinstance-detail", read_only=True 

+

208 ) 

+

209 

+

210 class Meta: 

+

211 model = Exercise 

+

212 fields = ["url", "id", "name", "description", "unit", "instances"] 

+

213 

+
+ + + diff --git a/backend/htmlcov/z_8c45c045706a6dc0_urls_py.html b/backend/htmlcov/z_8c45c045706a6dc0_urls_py.html new file mode 100644 index 0000000..b0f3ea9 --- /dev/null +++ b/backend/htmlcov/z_8c45c045706a6dc0_urls_py.html @@ -0,0 +1,140 @@ + + + + + Coverage for workouts\urls.py: 100% + + + + + +
+
+

+ Coverage for workouts\urls.py: + 100% +

+ +

+ 4 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from django.urls import path, include 

+

2from . import views 

+

3from rest_framework.urlpatterns import format_suffix_patterns 

+

4 

+

5urlpatterns = format_suffix_patterns( 

+

6 [ 

+

7 path("", views.api_root), 

+

8 path("api/workouts/", views.WorkoutList.as_view(), name="workout-list"), 

+

9 path( 

+

10 "api/workouts/<int:pk>/", 

+

11 views.WorkoutDetail.as_view(), 

+

12 name="workout-detail", 

+

13 ), 

+

14 path("api/exercises/", views.ExerciseList.as_view(), name="exercise-list"), 

+

15 path( 

+

16 "api/exercises/<int:pk>/", 

+

17 views.ExerciseDetail.as_view(), 

+

18 name="exercise-detail", 

+

19 ), 

+

20 path( 

+

21 "api/exercise-instances/", 

+

22 views.ExerciseInstanceList.as_view(), 

+

23 name="exercise-instance-list", 

+

24 ), 

+

25 path( 

+

26 "api/exercise-instances/<int:pk>/", 

+

27 views.ExerciseInstanceDetail.as_view(), 

+

28 name="exerciseinstance-detail", 

+

29 ), 

+

30 path( 

+

31 "api/workout-files/", 

+

32 views.WorkoutFileList.as_view(), 

+

33 name="workout-file-list", 

+

34 ), 

+

35 path( 

+

36 "api/workout-files/<int:pk>/", 

+

37 views.WorkoutFileDetail.as_view(), 

+

38 name="workoutfile-detail", 

+

39 ), 

+

40 path("", include("users.urls")), 

+

41 path("", include("comments.urls")), 

+

42 ] 

+

43) 

+
+ + + diff --git a/backend/htmlcov/z_8c45c045706a6dc0_views_py.html b/backend/htmlcov/z_8c45c045706a6dc0_views_py.html new file mode 100644 index 0000000..3cfd3a9 --- /dev/null +++ b/backend/htmlcov/z_8c45c045706a6dc0_views_py.html @@ -0,0 +1,388 @@ + + + + + Coverage for workouts\views.py: 75% + + + + + +
+
+

+ Coverage for workouts\views.py: + 75% +

+ +

+ 114 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1"""Contains views for the workouts application. These are mostly class-based views. 

+

2""" 

+

3from rest_framework import generics, mixins 

+

4from rest_framework import permissions 

+

5 

+

6from rest_framework.parsers import ( 

+

7 JSONParser, 

+

8) 

+

9from rest_framework.decorators import api_view 

+

10from rest_framework.response import Response 

+

11from rest_framework.reverse import reverse 

+

12from django.db.models import Q 

+

13from rest_framework import filters 

+

14from .parsers import MultipartJsonParser 

+

15from .permissions import ( 

+

16 IsOwner, 

+

17 IsCoachAndVisibleToCoach, 

+

18 IsOwnerOfWorkout, 

+

19 IsCoachOfWorkoutAndVisibleToCoach, 

+

20 IsReadOnly, 

+

21 IsPublic, 

+

22 IsWorkoutPublic, 

+

23) 

+

24from .mixins import CreateListModelMixin 

+

25from .models import Workout, Exercise, ExerciseInstance, WorkoutFile 

+

26from .serializers import WorkoutSerializer, ExerciseSerializer 

+

27from .serializers import ExerciseInstanceSerializer, WorkoutFileSerializer 

+

28from rest_framework.response import Response 

+

29 

+

30 

+

31@api_view(["GET"]) 

+

32def api_root(request, format=None): 

+

33 return Response( 

+

34 { 

+

35 "users": reverse("user-list", request=request, format=format), 

+

36 "workouts": reverse("workout-list", request=request, format=format), 

+

37 "exercises": reverse("exercise-list", request=request, format=format), 

+

38 "exercise-instances": reverse( 

+

39 "exercise-instance-list", request=request, format=format 

+

40 ), 

+

41 "workout-files": reverse( 

+

42 "workout-file-list", request=request, format=format 

+

43 ), 

+

44 "comments": reverse("comment-list", request=request, format=format), 

+

45 "likes": reverse("like-list", request=request, format=format), 

+

46 } 

+

47 ) 

+

48 

+

49 

+

50class WorkoutList( 

+

51 mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView 

+

52): 

+

53 """Class defining the web response for the creation of a Workout, or displaying a list 

+

54 of Workouts 

+

55 

+

56 HTTP methods: GET, POST 

+

57 """ 

+

58 

+

59 serializer_class = WorkoutSerializer 

+

60 permission_classes = [ 

+

61 permissions.IsAuthenticated 

+

62 ] # User must be authenticated to create/view workouts 

+

63 parser_classes = [ 

+

64 MultipartJsonParser, 

+

65 JSONParser, 

+

66 ] # For parsing JSON and Multi-part requests 

+

67 filter_backends = [filters.OrderingFilter] 

+

68 ordering_fields = ["name", "date", "owner__username"] 

+

69 

+

70 def get(self, request, *args, **kwargs): 

+

71 return self.list(request, *args, **kwargs) 

+

72 

+

73 def post(self, request, *args, **kwargs): 

+

74 return self.create(request, *args, **kwargs) 

+

75 

+

76 def perform_create(self, serializer): 

+

77 serializer.save(owner=self.request.user) 

+

78 

+

79 def get_queryset(self): 

+

80 qs = Workout.objects.none() 

+

81 if self.request.user: 

+

82 # A workout should be visible to the requesting user if any of the following hold: 

+

83 # - The workout has public visibility 

+

84 # - The owner of the workout is the requesting user 

+

85 # - The workout has coach visibility and the requesting user is the owner's coach 

+

86 qs = Workout.objects.filter( 

+

87 Q(visibility="PU") 

+

88 | Q(owner=self.request.user) 

+

89 | (Q(visibility="CO") & Q(owner__coach=self.request.user)) 

+

90 ).distinct() 

+

91 

+

92 return qs 

+

93 

+

94 

+

95class WorkoutDetail( 

+

96 mixins.RetrieveModelMixin, 

+

97 mixins.UpdateModelMixin, 

+

98 mixins.DestroyModelMixin, 

+

99 generics.GenericAPIView, 

+

100): 

+

101 """Class defining the web response for the details of an individual Workout. 

+

102 

+

103 HTTP methods: GET, PUT, DELETE 

+

104 """ 

+

105 

+

106 queryset = Workout.objects.all() 

+

107 serializer_class = WorkoutSerializer 

+

108 permission_classes = [ 

+

109 permissions.IsAuthenticated 

+

110 & (IsOwner | (IsReadOnly & (IsCoachAndVisibleToCoach | IsPublic))) 

+

111 ] 

+

112 parser_classes = [MultipartJsonParser, JSONParser] 

+

113 

+

114 def get(self, request, *args, **kwargs): 

+

115 return self.retrieve(request, *args, **kwargs) 

+

116 

+

117 def put(self, request, *args, **kwargs): 

+

118 return self.update(request, *args, **kwargs) 

+

119 

+

120 def delete(self, request, *args, **kwargs): 

+

121 return self.destroy(request, *args, **kwargs) 

+

122 

+

123 

+

124class ExerciseList( 

+

125 mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView 

+

126): 

+

127 """Class defining the web response for the creation of an Exercise, or 

+

128 a list of Exercises. 

+

129 

+

130 HTTP methods: GET, POST 

+

131 """ 

+

132 

+

133 queryset = Exercise.objects.all() 

+

134 serializer_class = ExerciseSerializer 

+

135 permission_classes = [permissions.IsAuthenticated] 

+

136 

+

137 def get(self, request, *args, **kwargs): 

+

138 return self.list(request, *args, **kwargs) 

+

139 

+

140 def post(self, request, *args, **kwargs): 

+

141 return self.create(request, *args, **kwargs) 

+

142 

+

143 

+

144class ExerciseDetail( 

+

145 mixins.RetrieveModelMixin, 

+

146 mixins.UpdateModelMixin, 

+

147 mixins.DestroyModelMixin, 

+

148 generics.GenericAPIView, 

+

149): 

+

150 """Class defining the web response for the details of an individual Exercise. 

+

151 

+

152 HTTP methods: GET, PUT, PATCH, DELETE 

+

153 """ 

+

154 

+

155 queryset = Exercise.objects.all() 

+

156 serializer_class = ExerciseSerializer 

+

157 permission_classes = [permissions.IsAuthenticated] 

+

158 

+

159 def get(self, request, *args, **kwargs): 

+

160 return self.retrieve(request, *args, **kwargs) 

+

161 

+

162 def put(self, request, *args, **kwargs): 

+

163 return self.update(request, *args, **kwargs) 

+

164 

+

165 def patch(self, request, *args, **kwargs): 

+

166 return self.partial_update(request, *args, **kwargs) 

+

167 

+

168 def delete(self, request, *args, **kwargs): 

+

169 return self.destroy(request, *args, **kwargs) 

+

170 

+

171 

+

172class ExerciseInstanceList( 

+

173 mixins.ListModelMixin, 

+

174 mixins.CreateModelMixin, 

+

175 CreateListModelMixin, 

+

176 generics.GenericAPIView, 

+

177): 

+

178 """Class defining the web response for the creation""" 

+

179 

+

180 serializer_class = ExerciseInstanceSerializer 

+

181 permission_classes = [permissions.IsAuthenticated & IsOwnerOfWorkout] 

+

182 

+

183 def get(self, request, *args, **kwargs): 

+

184 return self.list(request, *args, **kwargs) 

+

185 

+

186 def post(self, request, *args, **kwargs): 

+

187 return self.create(request, *args, **kwargs) 

+

188 

+

189 def get_queryset(self): 

+

190 qs = ExerciseInstance.objects.none() 

+

191 if self.request.user: 

+

192 qs = ExerciseInstance.objects.filter( 

+

193 Q(workout__owner=self.request.user) 

+

194 | ( 

+

195 (Q(workout__visibility="CO") | Q(workout__visibility="PU")) 

+

196 & Q(workout__owner__coach=self.request.user) 

+

197 ) 

+

198 ).distinct() 

+

199 

+

200 return qs 

+

201 

+

202 

+

203class ExerciseInstanceDetail( 

+

204 mixins.RetrieveModelMixin, 

+

205 mixins.UpdateModelMixin, 

+

206 mixins.DestroyModelMixin, 

+

207 generics.GenericAPIView, 

+

208): 

+

209 serializer_class = ExerciseInstanceSerializer 

+

210 permission_classes = [ 

+

211 permissions.IsAuthenticated 

+

212 & ( 

+

213 IsOwnerOfWorkout 

+

214 | (IsReadOnly & (IsCoachOfWorkoutAndVisibleToCoach | IsWorkoutPublic)) 

+

215 ) 

+

216 ] 

+

217 

+

218 queryset = ExerciseInstance.objects.all() 

+

219 

+

220 def get(self, request, *args, **kwargs): 

+

221 return self.retrieve(request, *args, **kwargs) 

+

222 

+

223 def put(self, request, *args, **kwargs): 

+

224 return self.update(request, *args, **kwargs) 

+

225 

+

226 def patch(self, request, *args, **kwargs): 

+

227 return self.partial_update(request, *args, **kwargs) 

+

228 

+

229 def delete(self, request, *args, **kwargs): 

+

230 return self.destroy(request, *args, **kwargs) 

+

231 

+

232 

+

233class WorkoutFileList( 

+

234 mixins.ListModelMixin, 

+

235 mixins.CreateModelMixin, 

+

236 CreateListModelMixin, 

+

237 generics.GenericAPIView, 

+

238): 

+

239 

+

240 queryset = WorkoutFile.objects.all() 

+

241 serializer_class = WorkoutFileSerializer 

+

242 permission_classes = [permissions.IsAuthenticated & IsOwnerOfWorkout] 

+

243 parser_classes = [MultipartJsonParser, JSONParser] 

+

244 

+

245 def get(self, request, *args, **kwargs): 

+

246 return self.list(request, *args, **kwargs) 

+

247 

+

248 def post(self, request, *args, **kwargs): 

+

249 return self.create(request, *args, **kwargs) 

+

250 

+

251 def perform_create(self, serializer): 

+

252 serializer.save(owner=self.request.user) 

+

253 

+

254 def get_queryset(self): 

+

255 qs = WorkoutFile.objects.none() 

+

256 if self.request.user: 

+

257 qs = WorkoutFile.objects.filter( 

+

258 Q(owner=self.request.user) 

+

259 | Q(workout__owner=self.request.user) 

+

260 | ( 

+

261 Q(workout__visibility="CO") 

+

262 & Q(workout__owner__coach=self.request.user) 

+

263 ) 

+

264 ).distinct() 

+

265 

+

266 return qs 

+

267 

+

268 

+

269class WorkoutFileDetail( 

+

270 mixins.RetrieveModelMixin, 

+

271 mixins.UpdateModelMixin, 

+

272 mixins.DestroyModelMixin, 

+

273 generics.GenericAPIView, 

+

274): 

+

275 

+

276 queryset = WorkoutFile.objects.all() 

+

277 serializer_class = WorkoutFileSerializer 

+

278 permission_classes = [ 

+

279 permissions.IsAuthenticated 

+

280 & ( 

+

281 IsOwner 

+

282 | IsOwnerOfWorkout 

+

283 | (IsReadOnly & (IsCoachOfWorkoutAndVisibleToCoach | IsWorkoutPublic)) 

+

284 ) 

+

285 ] 

+

286 

+

287 def get(self, request, *args, **kwargs): 

+

288 return self.retrieve(request, *args, **kwargs) 

+

289 

+

290 def delete(self, request, *args, **kwargs): 

+

291 return self.destroy(request, *args, **kwargs) 

+
+ + + diff --git a/backend/htmlcov/z_a44f0ac069e85531___init___py.html b/backend/htmlcov/z_a44f0ac069e85531___init___py.html new file mode 100644 index 0000000..1c080a3 --- /dev/null +++ b/backend/htmlcov/z_a44f0ac069e85531___init___py.html @@ -0,0 +1,98 @@ + + + + + Coverage for tests\__init__.py: 100% + + + + + +
+
+

+ Coverage for tests\__init__.py: + 100% +

+ +

+ 0 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1#Marks this folder as a python project 

+
+ + + diff --git a/backend/htmlcov/z_a44f0ac069e85531_test_TC001_py.html b/backend/htmlcov/z_a44f0ac069e85531_test_TC001_py.html new file mode 100644 index 0000000..3689cd1 --- /dev/null +++ b/backend/htmlcov/z_a44f0ac069e85531_test_TC001_py.html @@ -0,0 +1,189 @@ + + + + + Coverage for tests\test_TC001.py: 100% + + + + + +
+
+

+ Coverage for tests\test_TC001.py: + 100% +

+ +

+ 26 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from rest_framework.test import APITestCase 

+

2from rest_framework import status 

+

3from django.urls import reverse 

+

4from workouts.models import Exercise 

+

5from django.contrib.auth import get_user_model 

+

6 

+

7class WorkoutRobustBoundaryTestCase(APITestCase): 

+

8 def setUp(self): 

+

9 # Create a user 

+

10 self.user = get_user_model().objects.create_user( 

+

11 username="test_user", password="password123" 

+

12 ) 

+

13 

+

14 # Authenticate the user 

+

15 self.client.force_authenticate(user=self.user) 

+

16 

+

17 # Create a valid exercise 

+

18 self.valid_exercise = Exercise.objects.create( 

+

19 name="Valid Exercise", description="A valid exercise", unit="reps" 

+

20 ) 

+

21 

+

22 def test_valid_workout_creation(self): 

+

23 """Test creating a workout with valid exercises.""" 

+

24 url = reverse('workout-list') 

+

25 valid_workout_data = { 

+

26 "name": "Valid Workout", 

+

27 "date": "2025-04-01T10:00:00Z", 

+

28 "notes": "This is a valid workout", 

+

29 "visibility": "PU", 

+

30 "exercise_instances": [ 

+

31 { 

+

32 "exercise": f"/api/exercises/{self.valid_exercise.id}/", 

+

33 "sets": 3, 

+

34 "number": 10 

+

35 } 

+

36 ] 

+

37 } 

+

38 

+

39 response = self.client.post(url, valid_workout_data, format='json') 

+

40 print(response.content) # For debugging purposes 

+

41 

+

42 # Assert that the workout was created successfully 

+

43 self.assertEqual(response.status_code, status.HTTP_201_CREATED) 

+

44 

+

45 def test_invalid_workout_creation_future_date(self): 

+

46 """Test creating a workout with an invalid future date.""" 

+

47 url = reverse('workout-list') 

+

48 invalid_workout_data = { 

+

49 "name": "Invalid Workout", 

+

50 "date": "2030-01-01T10:00:00Z", #Future date 

+

51 "notes": "This workout has an invalid future date", 

+

52 "visibility": "PU", 

+

53 "exercise_instances": [ 

+

54 { 

+

55 "exercise": f"/api/exercises/{self.valid_exercise.id}/", 

+

56 "sets": 3, 

+

57 "number": 10 

+

58 } 

+

59 ] 

+

60 } 

+

61 

+

62 response = self.client.post(url, invalid_workout_data, format='json') 

+

63 print(response.content) # For debugging purposes 

+

64 

+

65 # Assert that the workout creation fails due to invalid date 

+

66 #self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 

+

67 

+

68 def test_invalid_workout_creation_past_date(self): 

+

69 """Test creating a workout with an invalid future date.""" 

+

70 url = reverse('workout-list') 

+

71 invalid_workout_data = { 

+

72 "name": "Invalid Workout 2", 

+

73 "date": "1900-01-01T10:00:00Z", #Future date 

+

74 "notes": "This workout has an invalid past date - part 2", 

+

75 "visibility": "PU", 

+

76 "exercise_instances": [ 

+

77 { 

+

78 "exercise": f"/api/exercises/{self.valid_exercise.id}/", 

+

79 "sets": 3, 

+

80 "number": 10 

+

81 } 

+

82 ] 

+

83 } 

+

84 

+

85 

+

86 

+

87 

+

88 response = self.client.post(url, invalid_workout_data, format='json') 

+

89 print(response.content) # For debugging purposes 

+

90 

+

91 # Assert that the workout creation fails due to invalid date 

+

92 #self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 

+
+ + + diff --git a/backend/htmlcov/z_a44f0ac069e85531_test_TC002_py.html b/backend/htmlcov/z_a44f0ac069e85531_test_TC002_py.html new file mode 100644 index 0000000..db4e89b --- /dev/null +++ b/backend/htmlcov/z_a44f0ac069e85531_test_TC002_py.html @@ -0,0 +1,226 @@ + + + + + Coverage for tests\test_TC002.py: 100% + + + + + +
+
+

+ Coverage for tests\test_TC002.py: + 100% +

+ +

+ 43 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from rest_framework.test import APITestCase 

+

2from django.contrib.auth import get_user_model 

+

3from rest_framework import status 

+

4from users.models import Offer 

+

5 

+

6User = get_user_model() 

+

7 

+

8class AthleteCoachRequestTest(APITestCase): 

+

9 

+

10 def setUp(self): 

+

11 """Set up the test environment with an athlete and multiple coaches.""" 

+

12 self.athlete = User.objects.create_user(username="athlete", password="password") 

+

13 

+

14 self.coach1 = User.objects.create_user(username="coach1", password="password", isCoach=True) 

+

15 self.coach2 = User.objects.create_user(username="coach2", password="password", isCoach=True) 

+

16 

+

17 self.client.force_authenticate(user=self.athlete) # Authenticate as athlete 

+

18 

+

19 def test_athlete_initially_has_no_coach(self): 

+

20 """Assert that an athlete has no assigned coach initially.""" 

+

21 self.assertIsNone(self.athlete.coach) 

+

22 

+

23 def test_athlete_sends_request_to_coach_and_coach_accepts(self): 

+

24 """Athlete sends request to a coach, and the coach accepts.""" 

+

25 

+

26 # Athlete sends request to coach1 

+

27 response = self.client.post( 

+

28 "/api/offers/", 

+

29 { 

+

30 "owner": self.athlete.id, # Athlete is the owner 

+

31 "recipient": f"http://testserver/api/users/{self.coach1.id}/", # Coach is the recipient 

+

32 "status": "p", # 'p' for Pending 

+

33 }, 

+

34 format="json" 

+

35 ) 

+

36 self.assertEqual(response.status_code, status.HTTP_201_CREATED) # Request created 

+

37 

+

38 # Fetch offer object 

+

39 offer = Offer.objects.get(owner=self.athlete, recipient=self.coach1) 

+

40 

+

41 # Coach accepts the offer 

+

42 response = self.client.put( 

+

43 f"/api/offers/{offer.id}/", 

+

44 { 

+

45 "status": "a", # 'a' for Accepted 

+

46 "recipient": f"http://testserver/api/users/{self.coach1.id}/", 

+

47 }, 

+

48 ) 

+

49 self.assertEqual(response.status_code, 200) 

+

50 

+

51 # Verify that coach1 is assigned as the athlete's coach 

+

52 self.athlete.refresh_from_db() 

+

53 self.assertEqual(self.athlete.coach, self.coach1) 

+

54 

+

55 def test_athlete_sends_requests_to_multiple_coaches_and_last_accepting_coach_is_assigned(self): 

+

56 """Athlete sends requests to multiple coaches; the last accepting coach should be assigned.""" 

+

57 

+

58 # Athlete sends requests to multiple coaches 

+

59 for coach in [self.coach1, self.coach2]: 

+

60 response = self.client.post( 

+

61 "/api/offers/", 

+

62 { 

+

63 "owner": self.athlete.id, # Athlete is the owner 

+

64 "recipient": f"http://testserver/api/users/{coach.id}/", # Coach is the recipient 

+

65 "status": "p", # 'p' for Pending 

+

66 }, 

+

67 format="json" 

+

68 ) 

+

69 self.assertEqual(response.status_code, status.HTTP_201_CREATED) # Request created 

+

70 

+

71 # Coach1 accepts the offer 

+

72 offer1 = Offer.objects.get(owner=self.athlete, recipient=self.coach1) 

+

73 response = self.client.put( 

+

74 f"/api/offers/{offer1.id}/", 

+

75 { 

+

76 "status": "a", # Accepting the offer 

+

77 "recipient": f"http://testserver/api/users/{self.coach1.id}/", 

+

78 }, 

+

79 ) 

+

80 self.assertEqual(response.status_code, 200) 

+

81 

+

82 # Coach2 accepts the offer 

+

83 offer2 = Offer.objects.get(owner=self.athlete, recipient=self.coach2) 

+

84 response = self.client.put( 

+

85 f"/api/offers/{offer2.id}/", 

+

86 { 

+

87 "status": "a", # Accepting the offer 

+

88 "recipient": f"http://testserver/api/users/{self.coach2.id}/", 

+

89 }, 

+

90 ) 

+

91 self.assertEqual(response.status_code, 200) 

+

92 

+

93 # Verify that coach2 is assigned as the athlete's coach (last accepting coach) 

+

94 self.athlete.refresh_from_db() 

+

95 self.assertEqual(self.athlete.coach, self.coach2) 

+

96 

+

97 def test_multiple_requests_to_single_coach_all_other_requests_get_deleted_on_acceptance(self): 

+

98 """Athlete sends multiple requests to a single coach, only one gets accepted, others should be removed.""" 

+

99 

+

100 # Athlete sends multiple requests to coach1 

+

101 for _ in range(3): # Simulating multiple requests 

+

102 response = self.client.post( 

+

103 "/api/offers/", 

+

104 { 

+

105 "owner": self.athlete.id, # Athlete is the owner 

+

106 "recipient": f"http://testserver/api/users/{self.coach1.id}/", # Coach is the recipient 

+

107 "status": "p", # 'p' for Pending 

+

108 }, 

+

109 format="json" 

+

110 ) 

+

111 self.assertEqual(response.status_code, status.HTTP_201_CREATED) 

+

112 

+

113 # Fetch all offers by athlete to coach1 

+

114 all_offers = Offer.objects.filter(owner=self.athlete, recipient=self.coach1) 

+

115 self.assertGreater(len(all_offers), 1) # Ensure multiple requests exist 

+

116 

+

117 # Coach1 accepts one offer 

+

118 response = self.client.put( 

+

119 f"/api/offers/{all_offers[0].id}/", 

+

120 { 

+

121 "status": "a", # Accepting the offer 

+

122 "recipient": f"http://testserver/api/users/{self.coach1.id}/", 

+

123 }, 

+

124 ) 

+

125 self.assertEqual(response.status_code, 200) 

+

126 

+

127 # Verify that all other pending offers from athlete to coach1 were deleted 

+

128 remaining_offers = Offer.objects.filter(owner=self.athlete, recipient=self.coach1) 

+

129 self.assertEqual(len(remaining_offers), 1) # Only one accepted offer should remain 

+
+ + + diff --git a/backend/htmlcov/z_a44f0ac069e85531_test_TC003_py.html b/backend/htmlcov/z_a44f0ac069e85531_test_TC003_py.html new file mode 100644 index 0000000..59a1d09 --- /dev/null +++ b/backend/htmlcov/z_a44f0ac069e85531_test_TC003_py.html @@ -0,0 +1,165 @@ + + + + + Coverage for tests\test_TC003.py: 100% + + + + + +
+
+

+ Coverage for tests\test_TC003.py: + 100% +

+ +

+ 22 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from rest_framework.test import APITestCase 

+

2from rest_framework import status 

+

3from django.urls import reverse 

+

4from workouts.models import Exercise 

+

5from django.contrib.auth import get_user_model 

+

6 

+

7class WorkoutRobustBoundaryTestCase(APITestCase): 

+

8 def setUp(self): 

+

9 # Create a user 

+

10 self.user = get_user_model().objects.create_user( 

+

11 username="test_user", password="password123" 

+

12 ) 

+

13 

+

14 # Authenticate the user 

+

15 self.client.force_authenticate(user=self.user) 

+

16 

+

17 # Create a valid exercise 

+

18 self.valid_exercise = Exercise.objects.create( 

+

19 name="Valid Exercise", description="A valid exercise", unit="reps" 

+

20 ) 

+

21 

+

22 def test_valid_workout_creation(self): 

+

23 """Test creating a workout with valid exercises.""" 

+

24 url = reverse('workout-list') 

+

25 valid_workout_data = { 

+

26 "name": "Valid Workout", 

+

27 "date": "2025-04-01T10:00:00Z", 

+

28 "notes": "This is a valid workout", 

+

29 "visibility": "PU", 

+

30 "exercise_instances": [ 

+

31 { 

+

32 "exercise": f"/api/exercises/{self.valid_exercise.id}/", 

+

33 "sets": 3, 

+

34 "number": 10 

+

35 } 

+

36 ] 

+

37 } 

+

38 

+

39 response = self.client.post(url, valid_workout_data, format='json') 

+

40 print(response.content) # For debugging purposes 

+

41 

+

42 # Assert that the workout was created successfully 

+

43 self.assertEqual(response.status_code, status.HTTP_201_CREATED) 

+

44 

+

45 def test_invalid_workout_creation(self): 

+

46 """Test creating a workout with invalid exercises ie using boundary values.""" 

+

47 url = reverse('workout-list') 

+

48 invalid_workout_data = { 

+

49 "name": "Invalid Workout", 

+

50 "date": "2025-04-01T10:00:00Z", 

+

51 "notes": "This workout has boundary values for the exercises", 

+

52 "visibility": "PU", 

+

53 "exercise_instances": [ 

+

54 { 

+

55 "exercise": f"/api/exercises/{self.valid_exercise.id}/", 

+

56 "sets": -3, # Invalid: negative sets 

+

57 "number": -10 # Invalid: negative number 

+

58 } 

+

59 ] 

+

60 } 

+

61 

+

62 response = self.client.post(url, invalid_workout_data, format='json') 

+

63 print(response.content) # For debugging purposes 

+

64 

+

65 # Assert that the workout creation fails due to invalid exercise instances 

+

66 self.assertEqual(response.status_code, status.HTTP_201_CREATED) 

+

67 #self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 

+

68 

+
+ + + diff --git a/backend/htmlcov/z_a44f0ac069e85531_test_TC004_py.html b/backend/htmlcov/z_a44f0ac069e85531_test_TC004_py.html new file mode 100644 index 0000000..9012dcb --- /dev/null +++ b/backend/htmlcov/z_a44f0ac069e85531_test_TC004_py.html @@ -0,0 +1,179 @@ + + + + + Coverage for tests\test_TC004.py: 100% + + + + + +
+
+

+ Coverage for tests\test_TC004.py: + 100% +

+ +

+ 25 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1# This is for coverage testing independent of manage.py 

+

2''' 

+

3import os 

+

4import django 

+

5import sys 

+

6sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 

+

7os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'secfit.settings') 

+

8django.setup() 

+

9''' 

+

10 

+

11 

+

12from django.test import TestCase 

+

13from django.core.files.uploadedfile import SimpleUploadedFile 

+

14from users.models import AthleteFile 

+

15from workouts.models import Workout 

+

16from django.contrib.auth import get_user_model 

+

17from django.utils.timezone import now 

+

18 

+

19class TestSpecialCharacterFileName(TestCase): 

+

20 def setUp(self): 

+

21 # Create a test user (coach) 

+

22 User = get_user_model() 

+

23 self.coach = User.objects.create_user(username="test_coach", password="password123", isCoach=True) 

+

24 

+

25 # Create an athlete and assign the coach 

+

26 self.athlete = User.objects.create_user(username="test_athlete", password="password123", isCoach=False, coach=self.coach) 

+

27 

+

28 # Create a workout for the athlete 

+

29 self.workout = Workout.objects.create(owner=self.athlete, name="Test Workout", date=now()) 

+

30 

+

31 # Force authentication for the coach 

+

32 from rest_framework.test import APIClient 

+

33 self.client = APIClient() # Use APIClient for forced authentication 

+

34 self.client.force_authenticate(user=self.coach) 

+

35 

+

36 def test_upload_file_with_special_characters_in_name(self): 

+

37 """ 

+

38 Test uploading a file with special characters in its name. 

+

39 Expected behavior: 

+

40 1. The system should sanitize the file name by removing special characters and transforming spaces into underscores. 

+

41 2. A unique identifier should be appended to the sanitized name. 

+

42 3. The file should be saved in the database and associated with the correct owner and athlete. 

+

43 

+

44 Steps: 

+

45 1. Create a file with special characters in its name. 

+

46 2. Upload the file using the API. 

+

47 3. Verify the response status code is 201 (successful creation). 

+

48 4. Check that the file is saved in the database with the sanitized name. 

+

49 5. Verify the file is associated with the correct owner and athlete. 

+

50 """ 

+

51 # Create a file with special characters in its name 

+

52 special_char_file = SimpleUploadedFile("file@#$ %&()a.png", b"dummy content", content_type="image/png") 

+

53 

+

54 # Upload file 

+

55 response = self.client.post( 

+

56 "/api/athlete-files/", 

+

57 { 

+

58 "file": special_char_file, 

+

59 "workout": self.workout.id, 

+

60 "athlete": f"/api/users/{self.athlete.id}/", # Include the athlete field 

+

61 }, 

+

62 format="multipart" 

+

63 ) 

+

64 

+

65 self.assertEqual(response.status_code, 201) # Successful creation 

+

66 

+

67 # File was saved with a sanitized name and unique identifier? 

+

68 saved_file = AthleteFile.objects.first() 

+

69 self.assertIsNotNone(saved_file, "The file was not saved in the database.") 

+

70 

+

71 # Extract the base name of the file (without the directory path) 

+

72 saved_file_name = saved_file.file.name.split("/")[-1] 

+

73 

+

74 # Saved file name starts with "file" and ends with ".png"? 

+

75 self.assertTrue( 

+

76 saved_file_name.startswith("file_a") and saved_file_name.endswith(".png"), 

+

77 f"Expected file name to start with 'file_a' and end with '.png', but got '{saved_file_name}'." 

+

78 ) 

+

79 

+

80 # File is associated with the correct owner and athlete? 

+

81 self.assertEqual(saved_file.owner, self.coach) 

+

82 self.assertEqual(saved_file.athlete, self.athlete) 

+
+ + + diff --git a/backend/htmlcov/z_a44f0ac069e85531_test_TC005_py.html b/backend/htmlcov/z_a44f0ac069e85531_test_TC005_py.html new file mode 100644 index 0000000..ed4a885 --- /dev/null +++ b/backend/htmlcov/z_a44f0ac069e85531_test_TC005_py.html @@ -0,0 +1,164 @@ + + + + + Coverage for tests\test_TC005.py: 100% + + + + + +
+
+

+ Coverage for tests\test_TC005.py: + 100% +

+ +

+ 28 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from rest_framework.test import APITestCase, APIClient # Import APIClient 

+

2from django.contrib.auth import get_user_model 

+

3from workouts.models import Workout 

+

4from django.utils.timezone import now 

+

5 

+

6User = get_user_model() 

+

7 

+

8class TestCoachViewAthleteWorkouts(APITestCase): # Use APITestCase instead of TestCase 

+

9 def setUp(self): 

+

10 # Initialize the API client 

+

11 self.client = APIClient() 

+

12 

+

13 # Create coach and athlete 

+

14 self.coach = User.objects.create_user(username="test_coach", password="password123", isCoach=True) 

+

15 self.athlete = User.objects.create_user(username="test_athlete", password="password123", isCoach=False, coach=self.coach) 

+

16 

+

17 # Create workouts for the athlete 

+

18 self.workout1 = Workout.objects.create(name="Workout 1", owner=self.athlete, date=now()) 

+

19 self.workout2 = Workout.objects.create(name="Workout 2", owner=self.athlete, date=now()) 

+

20 

+

21 # Create another athlete not assigned to the coach 

+

22 self.other_athlete = User.objects.create_user(username="other_athlete", password="password123", isCoach=False) 

+

23 

+

24 # Create workouts for the other athlete 

+

25 self.other_workout1 = Workout.objects.create(name="Other Workout 1", owner=self.other_athlete, date=now()) 

+

26 self.other_workout2 = Workout.objects.create(name="Other Workout 2", owner=self.other_athlete, date=now()) 

+

27 

+

28 # Force authentication for the coach 

+

29 self.client.force_authenticate(user=self.coach) 

+

30 

+

31 def test_coach_can_view_assigned_athlete_workouts(self): 

+

32 ''' 

+

33 Test that the coach can view workouts assigned to their athlete. 

+

34 Expected behavior: 

+

35 1. The coach should be able to view all workouts assigned to their athlete. 

+

36 2. The workouts should be filtered based on the coach's ID. 

+

37 3. The response should include the correct workout details. 

+

38 Steps: 

+

39 1. Create a coach and an athlete. 

+

40 2. Assign workouts to the athlete. 

+

41 3. Create another athlete not assigned to the coach and assign workouts to them. 

+

42 4. Use coach's credentials to access the API endpoint for viewing workouts. 

+

43 5. Verify the response status code is 200. 

+

44 6. Check that the response contains only the workouts assigned to the coach's athlete. 

+

45 ''' 

+

46 # 4. Access the API endpoint to view workouts 

+

47 response = self.client.get("/api/workouts/") 

+

48 

+

49 # 5. Verify response status code 

+

50 self.assertEqual(response.status_code, 200, "The response status code is not 200") 

+

51 

+

52 # Check that the response contains the correct workout details 

+

53 response_data = response.json() 

+

54 workout_names = [workout["name"] for workout in response_data] 

+

55 

+

56 # Coach can see their athlete's workouts? 

+

57 self.assertIn("Workout 1", workout_names, "Workout 1 is not in the response") 

+

58 self.assertIn("Workout 2", workout_names, "Workout 2 is not in the response") 

+

59 

+

60 # Coach cannot see workouts of other athletes? 

+

61 self.assertNotIn("Other Workout 1", workout_names, "Other Workout 1 should not be in the response") 

+

62 self.assertNotIn("Other Workout 2", workout_names, "Other Workout 2 should not be in the response") 

+

63 

+

64 # Workouts are filtered based on the coach's ID? 

+

65 for workout in response_data: 

+

66 owner_id = int(workout["owner"].split("/")[-2]) 

+

67 self.assertEqual(owner_id, self.athlete.id, "The workout is not associated with the correct athlete") 

+
+ + + diff --git a/backend/htmlcov/z_a5b840cb18d5964e_0001_initial_py.html b/backend/htmlcov/z_a5b840cb18d5964e_0001_initial_py.html new file mode 100644 index 0000000..81eea87 --- /dev/null +++ b/backend/htmlcov/z_a5b840cb18d5964e_0001_initial_py.html @@ -0,0 +1,157 @@ + + + + + Coverage for workouts\migrations\0001_initial.py: 100% + + + + + +
+
+

+ Coverage for workouts\migrations\0001_initial.py: + 100% +

+ +

+ 8 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1# Generated by Django 4.0.8 on 2024-07-29 12:05 

+

2 

+

3from django.conf import settings 

+

4from django.db import migrations, models 

+

5import django.db.models.deletion 

+

6import workouts.models 

+

7 

+

8 

+

9class Migration(migrations.Migration): 

+

10 

+

11 initial = True 

+

12 

+

13 dependencies = [ 

+

14 migrations.swappable_dependency(settings.AUTH_USER_MODEL), 

+

15 ] 

+

16 

+

17 operations = [ 

+

18 migrations.CreateModel( 

+

19 name='Exercise', 

+

20 fields=[ 

+

21 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 

+

22 ('name', models.CharField(max_length=100)), 

+

23 ('description', models.TextField()), 

+

24 ('unit', models.CharField(max_length=50)), 

+

25 ], 

+

26 ), 

+

27 migrations.CreateModel( 

+

28 name='Workout', 

+

29 fields=[ 

+

30 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 

+

31 ('name', models.CharField(max_length=100)), 

+

32 ('date', models.DateTimeField()), 

+

33 ('notes', models.TextField()), 

+

34 ('visibility', models.CharField(choices=[('PU', 'Public'), ('CO', 'Coach'), ('PR', 'Private')], default='CO', max_length=2)), 

+

35 ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workouts', to=settings.AUTH_USER_MODEL)), 

+

36 ], 

+

37 options={ 

+

38 'ordering': ['-date'], 

+

39 }, 

+

40 ), 

+

41 migrations.CreateModel( 

+

42 name='WorkoutFile', 

+

43 fields=[ 

+

44 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 

+

45 ('file', models.FileField(upload_to=workouts.models.workout_directory_path)), 

+

46 ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workout_files', to=settings.AUTH_USER_MODEL)), 

+

47 ('workout', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='workouts.workout')), 

+

48 ], 

+

49 ), 

+

50 migrations.CreateModel( 

+

51 name='ExerciseInstance', 

+

52 fields=[ 

+

53 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 

+

54 ('sets', models.IntegerField()), 

+

55 ('number', models.IntegerField()), 

+

56 ('exercise', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='workouts.exercise')), 

+

57 ('workout', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exercise_instances', to='workouts.workout')), 

+

58 ], 

+

59 ), 

+

60 ] 

+
+ + + diff --git a/backend/htmlcov/z_a5b840cb18d5964e___init___py.html b/backend/htmlcov/z_a5b840cb18d5964e___init___py.html new file mode 100644 index 0000000..3341fbc --- /dev/null +++ b/backend/htmlcov/z_a5b840cb18d5964e___init___py.html @@ -0,0 +1,97 @@ + + + + + Coverage for workouts\migrations\__init__.py: 100% + + + + + +
+
+

+ Coverage for workouts\migrations\__init__.py: + 100% +

+ +

+ 0 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+
+ + + diff --git a/backend/htmlcov/z_c67bde34194b83d1___init___py.html b/backend/htmlcov/z_c67bde34194b83d1___init___py.html new file mode 100644 index 0000000..0790ee2 --- /dev/null +++ b/backend/htmlcov/z_c67bde34194b83d1___init___py.html @@ -0,0 +1,97 @@ + + + + + Coverage for secfit\__init__.py: 100% + + + + + +
+
+

+ Coverage for secfit\__init__.py: + 100% +

+ +

+ 0 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+
+ + + diff --git a/backend/htmlcov/z_c67bde34194b83d1_asgi_py.html b/backend/htmlcov/z_c67bde34194b83d1_asgi_py.html new file mode 100644 index 0000000..f968abe --- /dev/null +++ b/backend/htmlcov/z_c67bde34194b83d1_asgi_py.html @@ -0,0 +1,113 @@ + + + + + Coverage for secfit\asgi.py: 0% + + + + + +
+
+

+ Coverage for secfit\asgi.py: + 0% +

+ +

+ 4 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1""" 

+

2ASGI config for secfit project. 

+

3 

+

4It exposes the ASGI callable as a module-level variable named ``application``. 

+

5 

+

6For more information on this file, see 

+

7https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ 

+

8""" 

+

9 

+

10import os 

+

11 

+

12from django.core.asgi import get_asgi_application 

+

13 

+

14os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'secfit.settings') 

+

15 

+

16application = get_asgi_application() 

+
+ + + diff --git a/backend/htmlcov/z_c67bde34194b83d1_settings_py.html b/backend/htmlcov/z_c67bde34194b83d1_settings_py.html new file mode 100644 index 0000000..c941ffa --- /dev/null +++ b/backend/htmlcov/z_c67bde34194b83d1_settings_py.html @@ -0,0 +1,267 @@ + + + + + Coverage for secfit\settings.py: 100% + + + + + +
+
+

+ Coverage for secfit\settings.py: + 100% +

+ +

+ 30 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1""" 

+

2Django settings for secfit project. 

+

3 

+

4Generated by 'django-admin startproject' using Django 4.1. 

+

5 

+

6For more information on this file, see 

+

7https://docs.djangoproject.com/en/4.1/topics/settings/ 

+

8 

+

9For the full list of settings and their values, see 

+

10https://docs.djangoproject.com/en/4.1/ref/settings/ 

+

11""" 

+

12 

+

13from datetime import timedelta 

+

14from pathlib import Path 

+

15import os 

+

16 

+

17# Build paths inside the project like this: BASE_DIR / 'subdir'. 

+

18BASE_DIR = Path(__file__).resolve().parent.parent 

+

19 

+

20MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 

+

21MEDIA_URL = '/media/' 

+

22# Quick-start development settings - unsuitable for production 

+

23# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ 

+

24 

+

25# SECURITY WARNING: keep the secret key used in production secret! 

+

26SECRET_KEY = 'django-insecure-*=))=v-+@_c-6(-60o%nv2b^a8br%$)%k+u+%(9ayozs79)abc' 

+

27 

+

28# SECURITY WARNING: don't run with debug turned on in production! 

+

29DEBUG = True 

+

30 

+

31ALLOWED_HOSTS = ["0.0.0.0", "localhost", "127.0.0.1", ".idi.ntnu.no"] 

+

32 

+

33 

+

34# Application definition 

+

35 

+

36INSTALLED_APPS = [ 

+

37 'django.contrib.admin', 

+

38 'django.contrib.auth', 

+

39 'django.contrib.contenttypes', 

+

40 'django.contrib.sessions', 

+

41 'django.contrib.messages', 

+

42 'django.contrib.staticfiles', 

+

43 "workouts.apps.WorkoutsConfig", 

+

44 "users.apps.UsersConfig", 

+

45 "comments.apps.CommentsConfig", 

+

46 'rest_framework', 

+

47 'rest_framework_simplejwt.token_blacklist', 

+

48 'corsheaders', 

+

49] 

+

50 

+

51MIDDLEWARE = [ 

+

52 'corsheaders.middleware.CorsMiddleware', 

+

53 'django.middleware.security.SecurityMiddleware', 

+

54 'django.contrib.sessions.middleware.SessionMiddleware', 

+

55 'django.middleware.common.CommonMiddleware', 

+

56 'django.contrib.auth.middleware.AuthenticationMiddleware', 

+

57 'django.contrib.messages.middleware.MessageMiddleware', 

+

58 'django.middleware.clickjacking.XFrameOptionsMiddleware', 

+

59] 

+

60 

+

61CORS_ORIGIN_ALLOW_ALL = ( 

+

62 True 

+

63) 

+

64CORS_ORIGIN_WHITELIST = [ 

+

65 'http://localhost:3000', 

+

66] 

+

67CORS_ALLOW_METHODS = [ 

+

68 'DELETE', 

+

69 'GET', 

+

70 'OPTIONS', 

+

71 'PATCH', 

+

72 'POST', 

+

73 'PUT', 

+

74] 

+

75ROOT_URLCONF = 'secfit.urls' 

+

76AUTH_USER_MODEL = 'users.User' 

+

77 

+

78AUTHENTICATION_BACKENDS = [ 

+

79 'users.auth_backend.UsernameAuthBackend' 

+

80] 

+

81 

+

82REST_FRAMEWORK = { 

+

83 # Disabled default pagination 

+

84 "DEFAULT_PAGINATION_CLASS": None, 

+

85 "PAGE_SIZE": None, 

+

86 "DEFAULT_AUTHENTICATION_CLASSES": ( 

+

87 "rest_framework_simplejwt.authentication.JWTAuthentication", 

+

88 ), 

+

89} 

+

90 

+

91TEMPLATES = [ 

+

92 { 

+

93 'BACKEND': 'django.template.backends.django.DjangoTemplates', 

+

94 'DIRS': [], 

+

95 'APP_DIRS': True, 

+

96 'OPTIONS': { 

+

97 'context_processors': [ 

+

98 'django.template.context_processors.debug', 

+

99 'django.template.context_processors.request', 

+

100 'django.contrib.auth.context_processors.auth', 

+

101 'django.contrib.messages.context_processors.messages', 

+

102 ], 

+

103 }, 

+

104 }, 

+

105] 

+

106 

+

107WSGI_APPLICATION = 'secfit.wsgi.application' 

+

108 

+

109 

+

110# Database 

+

111# https://docs.djangoproject.com/en/4.1/ref/settings/#databases 

+

112 

+

113DATABASES = { 

+

114 'default': { 

+

115 'ENGINE': 'django.db.backends.sqlite3', 

+

116 'NAME': BASE_DIR / 'db.sqlite3', 

+

117 } 

+

118} 

+

119 

+

120 

+

121# Password validation 

+

122# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators 

+

123 

+

124AUTH_PASSWORD_VALIDATORS = [ 

+

125 { 

+

126 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 

+

127 }, 

+

128 { 

+

129 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 

+

130 }, 

+

131 { 

+

132 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 

+

133 }, 

+

134 { 

+

135 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 

+

136 }, 

+

137] 

+

138 

+

139PASSWORD_HASHERS = [ 

+

140 'django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher', 

+

141] 

+

142 

+

143 

+

144# Internationalization 

+

145# https://docs.djangoproject.com/en/4.1/topics/i18n/ 

+

146 

+

147LANGUAGE_CODE = 'en-us' 

+

148 

+

149TIME_ZONE = 'UTC' 

+

150 

+

151USE_I18N = True 

+

152 

+

153USE_TZ = True 

+

154 

+

155 

+

156# Static files (CSS, JavaScript, Images) 

+

157# https://docs.djangoproject.com/en/4.1/howto/static-files/ 

+

158 

+

159STATIC_URL = 'static/' 

+

160 

+

161# Default primary key field type 

+

162# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field 

+

163 

+

164DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 

+

165 

+

166SIMPLE_JWT = { 

+

167 'ACCESS_TOKEN_LIFETIME': timedelta(hours=72), 

+

168 'REFRESH_TOKEN_LIFETIME': timedelta(days=60), 

+

169 'ROTATE_REFRESH_TOKENS': True, 

+

170} 

+
+ + + diff --git a/backend/htmlcov/z_c67bde34194b83d1_urls_py.html b/backend/htmlcov/z_c67bde34194b83d1_urls_py.html new file mode 100644 index 0000000..76c6652 --- /dev/null +++ b/backend/htmlcov/z_c67bde34194b83d1_urls_py.html @@ -0,0 +1,126 @@ + + + + + Coverage for secfit\urls.py: 100% + + + + + +
+
+

+ Coverage for secfit\urls.py: + 100% +

+ +

+ 7 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1"""secfit URL Configuration 

+

2 

+

3The `urlpatterns` list routes URLs to views. For more information please see: 

+

4 https://docs.djangoproject.com/en/4.1/topics/http/urls/ 

+

5Examples: 

+

6Function views 

+

7 1. Add an import: from my_app import views 

+

8 2. Add a URL to urlpatterns: path('', views.home, name='home') 

+

9Class-based views 

+

10 1. Add an import: from other_app.views import Home 

+

11 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 

+

12Including another URLconf 

+

13 1. Import the include() function: from django.urls import include, path 

+

14 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 

+

15""" 

+

16from django.contrib import admin 

+

17from django.urls import path,include 

+

18from django.conf.urls.static import static 

+

19from django.conf import settings 

+

20from rest_framework.routers import DefaultRouter 

+

21 

+

22router = DefaultRouter() 

+

23 

+

24urlpatterns = [ 

+

25 path('admin/', admin.site.urls), 

+

26 path('api/', include(router.urls)), # DRF API root 

+

27 path('',include('workouts.urls')), 

+

28 

+

29] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 

+
+ + + diff --git a/backend/htmlcov/z_c67bde34194b83d1_wsgi_py.html b/backend/htmlcov/z_c67bde34194b83d1_wsgi_py.html new file mode 100644 index 0000000..4a65462 --- /dev/null +++ b/backend/htmlcov/z_c67bde34194b83d1_wsgi_py.html @@ -0,0 +1,113 @@ + + + + + Coverage for secfit\wsgi.py: 0% + + + + + +
+
+

+ Coverage for secfit\wsgi.py: + 0% +

+ +

+ 4 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1""" 

+

2WSGI config for secfit project. 

+

3 

+

4It exposes the WSGI callable as a module-level variable named ``application``. 

+

5 

+

6For more information on this file, see 

+

7https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ 

+

8""" 

+

9 

+

10import os 

+

11 

+

12from django.core.wsgi import get_wsgi_application 

+

13 

+

14os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'secfit.settings') 

+

15 

+

16application = get_wsgi_application() 

+
+ + + diff --git a/backend/htmlcov/z_f6c68bdc9becfc1e___init___py.html b/backend/htmlcov/z_f6c68bdc9becfc1e___init___py.html new file mode 100644 index 0000000..6b86824 --- /dev/null +++ b/backend/htmlcov/z_f6c68bdc9becfc1e___init___py.html @@ -0,0 +1,97 @@ + + + + + Coverage for users\__init__.py: 100% + + + + + +
+
+

+ Coverage for users\__init__.py: + 100% +

+ +

+ 0 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+
+ + + diff --git a/backend/htmlcov/z_f6c68bdc9becfc1e_admin_py.html b/backend/htmlcov/z_f6c68bdc9becfc1e_admin_py.html new file mode 100644 index 0000000..c28bf02 --- /dev/null +++ b/backend/htmlcov/z_f6c68bdc9becfc1e_admin_py.html @@ -0,0 +1,114 @@ + + + + + Coverage for users\admin.py: 100% + + + + + +
+
+

+ Coverage for users\admin.py: + 100% +

+ +

+ 14 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from django.contrib import admin 

+

2from django.contrib.auth.admin import UserAdmin 

+

3from .models import Offer, AthleteFile 

+

4from django.contrib.auth import get_user_model 

+

5from .forms import CustomUserChangeForm, CustomUserCreationForm 

+

6 

+

7class CustomUserAdmin(UserAdmin): 

+

8 add_form = CustomUserCreationForm 

+

9 form = CustomUserChangeForm 

+

10 model = get_user_model() 

+

11 fieldsets = UserAdmin.fieldsets + ((None, {"fields": ("coach","isCoach","specialism")}),) 

+

12 add_fieldsets = UserAdmin.add_fieldsets + ((None, {"fields": ("coach","isCoach","specialism")}),) 

+

13 

+

14 

+

15admin.site.register(get_user_model(), CustomUserAdmin) 

+

16admin.site.register(Offer) 

+

17admin.site.register(AthleteFile) 

+
+ + + diff --git a/backend/htmlcov/z_f6c68bdc9becfc1e_apps_py.html b/backend/htmlcov/z_f6c68bdc9becfc1e_apps_py.html new file mode 100644 index 0000000..ec966d2 --- /dev/null +++ b/backend/htmlcov/z_f6c68bdc9becfc1e_apps_py.html @@ -0,0 +1,102 @@ + + + + + Coverage for users\apps.py: 100% + + + + + +
+
+

+ Coverage for users\apps.py: + 100% +

+ +

+ 3 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from django.apps import AppConfig 

+

2 

+

3 

+

4class UsersConfig(AppConfig): 

+

5 name = "users" 

+
+ + + diff --git a/backend/htmlcov/z_f6c68bdc9becfc1e_auth_backend_py.html b/backend/htmlcov/z_f6c68bdc9becfc1e_auth_backend_py.html new file mode 100644 index 0000000..466c2bd --- /dev/null +++ b/backend/htmlcov/z_f6c68bdc9becfc1e_auth_backend_py.html @@ -0,0 +1,114 @@ + + + + + Coverage for users\auth_backend.py: 0% + + + + + +
+
+

+ Coverage for users\auth_backend.py: + 0% +

+ +

+ 15 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from django.contrib.auth import get_user_model 

+

2User = get_user_model() 

+

3 

+

4class UsernameAuthBackend: 

+

5 def authenticate(self,request, username=None, password=None): 

+

6 try: 

+

7 user = User.objects.get(username=username) 

+

8 if user.check_password(password): 

+

9 return user 

+

10 except User.DoesNotExist: 

+

11 return None 

+

12 

+

13 def get_user(self,user_id): 

+

14 try: 

+

15 return User.objects.get(pk=user_id) 

+

16 except User.DoesNotExist: 

+

17 return None 

+
+ + + diff --git a/backend/htmlcov/z_f6c68bdc9becfc1e_forms_py.html b/backend/htmlcov/z_f6c68bdc9becfc1e_forms_py.html new file mode 100644 index 0000000..243e7fc --- /dev/null +++ b/backend/htmlcov/z_f6c68bdc9becfc1e_forms_py.html @@ -0,0 +1,112 @@ + + + + + Coverage for users\forms.py: 100% + + + + + +
+
+

+ Coverage for users\forms.py: + 100% +

+ +

+ 11 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from django import forms 

+

2from django.contrib.auth.forms import UserCreationForm, UserChangeForm 

+

3from django.contrib.auth import get_user_model 

+

4 

+

5 

+

6class CustomUserCreationForm(UserCreationForm): 

+

7 class Meta(UserCreationForm): 

+

8 model = get_user_model() 

+

9 fields = ("username", "coach") 

+

10 

+

11 

+

12class CustomUserChangeForm(UserChangeForm): 

+

13 class Meta: 

+

14 model = get_user_model() 

+

15 fields = ("username", "coach") 

+
+ + + diff --git a/backend/htmlcov/z_f6c68bdc9becfc1e_models_py.html b/backend/htmlcov/z_f6c68bdc9becfc1e_models_py.html new file mode 100644 index 0000000..c8d3b60 --- /dev/null +++ b/backend/htmlcov/z_f6c68bdc9becfc1e_models_py.html @@ -0,0 +1,171 @@ + + + + + Coverage for users\models.py: 100% + + + + + +
+
+

+ Coverage for users\models.py: + 100% +

+ +

+ 23 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from django.db import models 

+

2from django.contrib.auth.models import AbstractUser 

+

3from django.contrib.auth import get_user_model 

+

4from .validators import FileValidator 

+

5 

+

6 

+

7class User(AbstractUser): 

+

8 """ 

+

9 Standard Django User model with an added field for a user's coach. 

+

10 """ 

+

11 isCoach = models.fields.BooleanField(default=False) 

+

12 coach = models.ForeignKey( 

+

13 "self", on_delete=models.CASCADE, related_name="athletes", blank=True, null=True 

+

14 ) 

+

15 specialism = models.fields.CharField(max_length=1000,default="") 

+

16 

+

17 

+

18 

+

19def athlete_directory_path(instance, filename): 

+

20 """ 

+

21 Return the path for an athlete's file 

+

22 :param instance: Current instance containing an athlete 

+

23 :param filename: Name of the file 

+

24 :return: Path of file as a string 

+

25 """ 

+

26 return f"users/{instance.athlete.id}/{filename}" 

+

27 

+

28 

+

29class AthleteFile(models.Model): 

+

30 """ 

+

31 Model for an athlete's file. Contains fields for the athlete for whom this file was uploaded, 

+

32 the coach owner, and the file itself. 

+

33 """ 

+

34 

+

35 athlete = models.ForeignKey( 

+

36 get_user_model(), on_delete=models.CASCADE, related_name="coach_files" 

+

37 ) 

+

38 owner = models.ForeignKey( 

+

39 get_user_model(), on_delete=models.CASCADE, related_name="athlete_files" 

+

40 ) 

+

41 file = models.FileField(upload_to=athlete_directory_path, validators=[FileValidator( 

+

42 allowed_mimetypes='', allowed_extensions='', max_size=1024*1024*5)] 

+

43 ) 

+

44 

+

45 

+

46class Offer(models.Model): 

+

47 """Django model for a coaching offer that one user sends to another. 

+

48 

+

49 Each offer has an owner, a recipient, a status, and a timestamp. 

+

50 

+

51 Attributes: 

+

52 owner: Who sent the offer 

+

53 recipient: Who received the offer 

+

54 status: The current status of the offer (accept, declined, or pending) 

+

55 timestamp: When the offer was sent. 

+

56 """ 

+

57 owner = models.ForeignKey( 

+

58 get_user_model(), on_delete=models.CASCADE, related_name="sent_offers" 

+

59 ) 

+

60 recipient = models.ForeignKey( 

+

61 get_user_model(), on_delete=models.CASCADE, related_name="received_offers" 

+

62 ) 

+

63 

+

64 ACCEPTED = "a" 

+

65 PENDING = "p" 

+

66 DECLINED = "d" 

+

67 STATUS_CHOICES = ( 

+

68 (ACCEPTED, "Accepted"), 

+

69 (PENDING, "Pending"), 

+

70 (DECLINED, "Declined"), 

+

71 ) 

+

72 

+

73 status = models.CharField(max_length=8, choices=STATUS_CHOICES, default=PENDING) 

+

74 timestamp = models.DateTimeField(auto_now_add=True) 

+
+ + + diff --git a/backend/htmlcov/z_f6c68bdc9becfc1e_permissions_py.html b/backend/htmlcov/z_f6c68bdc9becfc1e_permissions_py.html new file mode 100644 index 0000000..93824a4 --- /dev/null +++ b/backend/htmlcov/z_f6c68bdc9becfc1e_permissions_py.html @@ -0,0 +1,148 @@ + + + + + Coverage for users\permissions.py: 58% + + + + + +
+
+

+ Coverage for users\permissions.py: + 58% +

+ +

+ 36 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from rest_framework import permissions 

+

2from django.contrib.auth import get_user_model 

+

3 

+

4 

+

5class IsCurrentUser(permissions.BasePermission): 

+

6 def has_object_permission(self, request, view, obj): 

+

7 return obj == request.user 

+

8 

+

9 

+

10class IsAthlete(permissions.BasePermission): 

+

11 def has_permission(self, request, view): 

+

12 if request.method == "POST": 

+

13 if request.data.get("athlete"): 

+

14 athlete_id = request.data["athlete"].split("/")[-2] 

+

15 return athlete_id == request.user.id 

+

16 return False 

+

17 

+

18 return True 

+

19 

+

20 def has_object_permission(self, request, view, obj): 

+

21 return request.user == obj.athlete 

+

22 

+

23 

+

24class IsCoach(permissions.BasePermission): 

+

25 def has_permission(self, request, view): 

+

26 if request.method == "POST": 

+

27 if request.data.get("athlete"): 

+

28 athlete_id = request.data["athlete"].split("/")[-2] 

+

29 athlete = get_user_model().objects.get(pk=athlete_id) 

+

30 return athlete.coach == request.user 

+

31 return False 

+

32 

+

33 return True 

+

34 

+

35 def has_object_permission(self, request, view, obj): 

+

36 return request.user == obj.athlete.coach 

+

37 

+

38 

+

39class IsRecipientOfOffer(permissions.BasePermission): 

+

40 """Checks whether the user is the recipient of the offer""" 

+

41 

+

42 def has_permission(self, request, view): 

+

43 if request.method == "PUT": 

+

44 if request.data.get("recipient"): 

+

45 recipient_id = request.data["recipient"].split("/")[-2] 

+

46 recipient = get_user_model().objects.get(pk=recipient_id) 

+

47 if recipient: 

+

48 return recipient == request.user 

+

49 return False 

+

50 

+

51 return True 

+
+ + + diff --git a/backend/htmlcov/z_f6c68bdc9becfc1e_serializers_py.html b/backend/htmlcov/z_f6c68bdc9becfc1e_serializers_py.html new file mode 100644 index 0000000..f369285 --- /dev/null +++ b/backend/htmlcov/z_f6c68bdc9becfc1e_serializers_py.html @@ -0,0 +1,211 @@ + + + + + Coverage for users\serializers.py: 58% + + + + + +
+
+

+ Coverage for users\serializers.py: + 58% +

+ +

+ 57 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from rest_framework import serializers 

+

2from django.contrib.auth import get_user_model, password_validation 

+

3from .models import Offer, AthleteFile 

+

4from django import forms 

+

5 

+

6 

+

7class UserSerializer(serializers.HyperlinkedModelSerializer): 

+

8 password = serializers.CharField(style={"input_type": "password"}, write_only=True) 

+

9 password1 = serializers.CharField(style={"input_type": "password"}, write_only=True) 

+

10 

+

11 class Meta: 

+

12 model = get_user_model() 

+

13 fields = [ 

+

14 "url", 

+

15 "id", 

+

16 "email", 

+

17 "username", 

+

18 "password", 

+

19 "password1", 

+

20 "athletes", 

+

21 "isCoach", 

+

22 "coach", 

+

23 "specialism", 

+

24 "workouts", 

+

25 "coach_files", 

+

26 "athlete_files", 

+

27 ] 

+

28 

+

29 def validate_password(self, value): 

+

30 data = self.get_initial() 

+

31 

+

32 password = data.get("password") 

+

33 password1 = data.get("password1") 

+

34 

+

35 try: 

+

36 password_validation.validate_password(password) 

+

37 except forms.ValidationError as error: 

+

38 raise serializers.ValidationError(error.messages) 

+

39 

+

40 if password != password1: 

+

41 raise serializers.ValidationError("Passwords must match!") 

+

42 

+

43 return value 

+

44 

+

45 def create(self, validated_data): 

+

46 username = validated_data["username"] 

+

47 email = validated_data["email"] 

+

48 isCoach = validated_data["isCoach"] 

+

49 if (isCoach): 

+

50 specialism = validated_data["specialism"] 

+

51 user_obj = get_user_model()(username=username, email=email,isCoach=isCoach,specialism=specialism) 

+

52 else: 

+

53 user_obj = get_user_model()(username=username, email=email,isCoach=isCoach) 

+

54 password = validated_data["password"] 

+

55 user_obj.set_password(password) 

+

56 user_obj.save() 

+

57 

+

58 return user_obj 

+

59 

+

60 

+

61class UserGetSerializer(serializers.HyperlinkedModelSerializer): 

+

62 class Meta: 

+

63 model = get_user_model() 

+

64 fields = [ 

+

65 "url", 

+

66 "id", 

+

67 "email", 

+

68 "username", 

+

69 "athletes", 

+

70 "isCoach", 

+

71 "specialism", 

+

72 "coach", 

+

73 "workouts", 

+

74 "coach_files", 

+

75 "athlete_files", 

+

76 ] 

+

77 

+

78 

+

79class UserPutSerializer(serializers.ModelSerializer): 

+

80 class Meta: 

+

81 model = get_user_model() 

+

82 fields = ["athletes"] 

+

83 

+

84 def update(self, instance, validated_data): 

+

85 athletes_data = validated_data["athletes"] 

+

86 instance.athletes.set(athletes_data) 

+

87 

+

88 return instance 

+

89 

+

90 

+

91class AthleteFileSerializer(serializers.HyperlinkedModelSerializer): 

+

92 owner = serializers.ReadOnlyField(source="owner.username") 

+

93 

+

94 class Meta: 

+

95 model = AthleteFile 

+

96 fields = ["url", "id", "owner", "file", "athlete"] 

+

97 

+

98 def create(self, validated_data): 

+

99 return AthleteFile.objects.create(**validated_data) 

+

100 

+

101 

+

102class OfferSerializer(serializers.HyperlinkedModelSerializer): 

+

103 owner = serializers.ReadOnlyField(source="owner.username") 

+

104 

+

105 class Meta: 

+

106 model = Offer 

+

107 fields = [ 

+

108 "url", 

+

109 "id", 

+

110 "owner", 

+

111 "recipient", 

+

112 "status", 

+

113 "timestamp", 

+

114 ] 

+
+ + + diff --git a/backend/htmlcov/z_f6c68bdc9becfc1e_urls_py.html b/backend/htmlcov/z_f6c68bdc9becfc1e_urls_py.html new file mode 100644 index 0000000..4faed43 --- /dev/null +++ b/backend/htmlcov/z_f6c68bdc9becfc1e_urls_py.html @@ -0,0 +1,121 @@ + + + + + Coverage for users\urls.py: 100% + + + + + +
+
+

+ Coverage for users\urls.py: + 100% +

+ +

+ 4 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from django.urls import path, include 

+

2from . import views 

+

3from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView, TokenObtainPairView, TokenBlacklistView 

+

4 

+

5urlpatterns = [ 

+

6 path("api/users/", views.UserList.as_view(), name="user-list"), 

+

7 path("api/users/<int:pk>/", views.UserDetail.as_view(), name="user-detail"), 

+

8 path("api/users/<str:username>/", views.UserDetail.as_view(), name="user-detail"), 

+

9 path("api/offers/", views.OfferList.as_view(), name="offer-list"), 

+

10 path("api/offers/<int:pk>/", views.OfferDetail.as_view(), name="offer-detail"), 

+

11 path( 

+

12 "api/athlete-files/", views.AthleteFileList.as_view(), name="athlete-file-list" 

+

13 ), 

+

14 path( 

+

15 "api/athlete-files/<int:pk>/", 

+

16 views.AthleteFileDetail.as_view(), 

+

17 name="athletefile-detail", 

+

18 ), 

+

19 path("api/auth/", include("rest_framework.urls")), 

+

20 path('api/token/verify/', TokenVerifyView.as_view(), name='token_verify'), 

+

21 path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 

+

22 path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), 

+

23 path("api/logout/", TokenBlacklistView.as_view(), name="token_blacklist") 

+

24] 

+
+ + + diff --git a/backend/htmlcov/z_f6c68bdc9becfc1e_validators_py.html b/backend/htmlcov/z_f6c68bdc9becfc1e_validators_py.html new file mode 100644 index 0000000..cfd98c7 --- /dev/null +++ b/backend/htmlcov/z_f6c68bdc9becfc1e_validators_py.html @@ -0,0 +1,212 @@ + + + + + Coverage for users\validators.py: 60% + + + + + +
+
+

+ Coverage for users\validators.py: + 60% +

+ +

+ 43 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1# Retrieved from https://gist.github.com/mobula/da99e4db843b9ceb3a3f 

+

2# Modified to remove ImageValidator since it is not needed for this project. 

+

3 

+

4# @brief 

+

5# Performs file upload validation for django. 

+

6# with Django 1.7 migrations support (deconstructible) 

+

7 

+

8# @author dokterbob 

+

9# @author jrosebr1 

+

10# @author mobula 

+

11 

+

12import mimetypes 

+

13from os.path import splitext 

+

14 

+

15from django.core.exceptions import ValidationError 

+

16from django.utils.translation import gettext_lazy as _ 

+

17from django.template.defaultfilters import filesizeformat 

+

18 

+

19from django.utils.deconstruct import deconstructible 

+

20 

+

21 

+

22 

+

23@deconstructible 

+

24class FileValidator(object): 

+

25 """ 

+

26 Validator for files, checking the size, extension and mimetype. 

+

27 

+

28 Initialization parameters: 

+

29 allowed_extensions: iterable with allowed file extensions 

+

30 ie. ('txt', 'doc') 

+

31 allowed_mimetypes: iterable with allowed mimetypes 

+

32 ie. ('image/png', ) 

+

33 min_size: minimum number of bytes allowed 

+

34 ie. 100 

+

35 max_size: maximum number of bytes allowed 

+

36 ie. 24*1024*1024 for 24 MB 

+

37 

+

38 Usage example:: 

+

39 

+

40 MyModel(models.Model): 

+

41 myfile = FileField(validators=FileValidator(max_size=24*1024*1024), ...) 

+

42 

+

43 """ 

+

44 

+

45 messages = { 

+

46 'extension_not_allowed': _("Extension '%(extension)s' not allowed. Allowed extensions are: '%(allowed_extensions)s.'"), 

+

47 'mimetype_not_allowed': _("MIME type '%(mimetype)s' is not valid. Allowed types are: %(allowed_mimetypes)s."), 

+

48 'min_size': _('The current file %(size)s, which is too small. The minumum file size is %(allowed_size)s.'), 

+

49 'max_size': _('The current file %(size)s, which is too large. The maximum file size is %(allowed_size)s.') 

+

50 } 

+

51 

+

52 mime_message = _("MIME type '%(mimetype)s' is not valid. Allowed types are: %(allowed_mimetypes)s.") 

+

53 min_size_message = _('The current file %(size)s, which is too small. The minumum file size is %(allowed_size)s.') 

+

54 max_size_message = _('The current file %(size)s, which is too large. The maximum file size is %(allowed_size)s.') 

+

55 

+

56 def __init__(self, *args, **kwargs): 

+

57 self.allowed_extensions = kwargs.pop('allowed_extensions', None) 

+

58 self.allowed_mimetypes = kwargs.pop('allowed_mimetypes', None) 

+

59 self.min_size = kwargs.pop('min_size', 0) 

+

60 self.max_size = kwargs.pop('max_size', None) 

+

61 

+

62 def __eq__(self, other): 

+

63 return ( isinstance(other, FileValidator) 

+

64 and (self.allowed_extensions == other.allowed_extensions) 

+

65 and (self.allowed_mimetypes == other.allowed_mimetypes) 

+

66 and (self.min_size == other.min_size) 

+

67 and (self.max_size == other.max_size) 

+

68 ) 

+

69 

+

70 def __call__(self, value): 

+

71 """ 

+

72 Check the extension, content type and file size. 

+

73 """ 

+

74 

+

75 # Check the extension 

+

76 ext = splitext(value.name)[1][1:].lower() 

+

77 if self.allowed_extensions and not ext in self.allowed_extensions: 

+

78 code = 'extension_not_allowed' 

+

79 message = self.messages[code] 

+

80 params = { 

+

81 'extension' : ext, 

+

82 'allowed_extensions': ', '.join(self.allowed_extensions) 

+

83 } 

+

84 raise ValidationError(message=message, code=code, params=params) 

+

85 

+

86 # Check the content type 

+

87 mimetype = mimetypes.guess_type(value.name)[0] 

+

88 if self.allowed_mimetypes and not mimetype in self.allowed_mimetypes: 

+

89 code = 'mimetype_not_allowed' 

+

90 message = self.messages[code] 

+

91 params = { 

+

92 'mimetype': mimetype, 

+

93 'allowed_mimetypes': ', '.join(self.allowed_mimetypes) 

+

94 } 

+

95 raise ValidationError(message=message, code=code, params=params) 

+

96 

+

97 # Check the file size 

+

98 filesize = len(value) 

+

99 if self.max_size and filesize > self.max_size: 

+

100 code = 'max_size' 

+

101 message = self.messages[code] 

+

102 params = { 

+

103 'size': filesizeformat(filesize), 

+

104 'allowed_size': filesizeformat(self.max_size) 

+

105 } 

+

106 raise ValidationError(message=message, code=code, params=params) 

+

107 

+

108 elif filesize < self.min_size: 

+

109 code = 'min_size' 

+

110 message = self.messages[code] 

+

111 params = { 

+

112 'size': filesizeformat(filesize), 

+

113 'allowed_size': filesizeformat(self.min_size) 

+

114 } 

+

115 raise ValidationError(message=message, code=code, params=params) 

+
+ + + diff --git a/backend/htmlcov/z_f6c68bdc9becfc1e_views_py.html b/backend/htmlcov/z_f6c68bdc9becfc1e_views_py.html new file mode 100644 index 0000000..776a08d --- /dev/null +++ b/backend/htmlcov/z_f6c68bdc9becfc1e_views_py.html @@ -0,0 +1,339 @@ + + + + + Coverage for users\views.py: 56% + + + + + +
+
+

+ Coverage for users\views.py: + 56% +

+ +

+ 145 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1from rest_framework import mixins, generics 

+

2from workouts.mixins import CreateListModelMixin 

+

3from rest_framework import permissions 

+

4from users.serializers import ( 

+

5 UserSerializer, 

+

6 OfferSerializer, 

+

7 AthleteFileSerializer, 

+

8 UserPutSerializer, 

+

9 UserGetSerializer, 

+

10) 

+

11from rest_framework.permissions import ( 

+

12 IsAuthenticatedOrReadOnly, 

+

13) 

+

14 

+

15from .models import Offer, AthleteFile, User 

+

16from django.contrib.auth import get_user_model 

+

17from django.db import connection 

+

18from django.db.models import Q 

+

19from rest_framework.parsers import MultiPartParser, FormParser 

+

20from .permissions import IsCurrentUser, IsAthlete, IsCoach 

+

21from workouts.permissions import IsOwner, IsReadOnly 

+

22from rest_framework.response import Response 

+

23from rest_framework import status 

+

24 

+

25 

+

26class UserList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView): 

+

27 serializer_class = UserSerializer 

+

28 

+

29 def get(self, request, *args, **kwargs): 

+

30 self.serializer_class = UserGetSerializer 

+

31 return self.list(request, *args, **kwargs) 

+

32 

+

33 def post(self, request, *args, **kwargs): 

+

34 return self.create(request, *args, **kwargs) 

+

35 

+

36 def get_queryset(self): 

+

37 qs = get_user_model().objects.all() 

+

38 

+

39 if self.request.user: 

+

40 # Return the currently logged in user 

+

41 status = self.request.query_params.get("user", None) 

+

42 if status and status == "current": 

+

43 qs = get_user_model().objects.filter(pk=self.request.user.pk) 

+

44 

+

45 return qs 

+

46 

+

47 

+

48class UserDetail( 

+

49 mixins.RetrieveModelMixin, 

+

50 mixins.UpdateModelMixin, 

+

51 mixins.DestroyModelMixin, 

+

52 generics.GenericAPIView, 

+

53): 

+

54 lookup_field_options = ["pk", "username"] 

+

55 serializer_class = UserSerializer 

+

56 queryset = get_user_model().objects.all() 

+

57 permission_classes = [permissions.IsAuthenticated & 

+

58 (IsCurrentUser | IsReadOnly)] 

+

59 

+

60 def get_object(self): 

+

61 for field in self.lookup_field_options: 

+

62 if field in self.kwargs: 

+

63 self.lookup_field = field 

+

64 break 

+

65 

+

66 return super().get_object() 

+

67 

+

68 def get(self, request, *args, **kwargs): 

+

69 pk = kwargs.get('pk') 

+

70 username = kwargs.get('username') 

+

71 

+

72 if not pk and not username: 

+

73 return Response({'error': 'User ID or username not provided'}, status=400) 

+

74 

+

75 if pk: 

+

76 instance = self.get_object() 

+

77 serializer = self.get_serializer(instance) 

+

78 return Response(serializer.data) 

+

79 

+

80 

+

81 query = f"SELECT * FROM users_user WHERE username = '{username}'" 

+

82 with connection.cursor() as cursor: 

+

83 cursor.execute(query) # Executing the raw SQL query 

+

84 columns = [col[0] for col in cursor.description] 

+

85 rows = cursor.fetchall() 

+

86 

+

87 if not rows: 

+

88 return Response({'error': 'User not found'}, status=404) 

+

89 

+

90 instances = [] 

+

91 for row in rows: 

+

92 if row: 

+

93 data = dict(zip(columns, row)) 

+

94 instance = User(**data) 

+

95 instances.append(instance) 

+

96 

+

97 if len(instances) == 1: 

+

98 serializer = self.get_serializer( 

+

99 instances[0], context={'request': request}) 

+

100 else: 

+

101 serializer = self.get_serializer( 

+

102 instances, many=True, context={'request': request}) 

+

103 

+

104 return Response(serializer.data) 

+

105 

+

106 def delete(self, request, *args, **kwargs): 

+

107 return self.destroy(request, *args, **kwargs) 

+

108 

+

109 def put(self, request, *args, **kwargs): 

+

110 self.serializer_class = UserPutSerializer 

+

111 return self.update(request, *args, **kwargs) 

+

112 

+

113 def patch(self, request, *args, **kwargs): 

+

114 return self.partial_update(request, *args, **kwargs) 

+

115 

+

116 

+

117class OfferList( 

+

118 mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView 

+

119): 

+

120 permission_classes = [IsAuthenticatedOrReadOnly] 

+

121 serializer_class = OfferSerializer 

+

122 

+

123 def get(self, request, *args, **kwargs): 

+

124 return self.list(request, *args, **kwargs) 

+

125 

+

126 def post(self, request, *args, **kwargs): 

+

127 serializer = self.get_serializer(data=request.data) 

+

128 serializer.is_valid(raise_exception=True) 

+

129 self.perform_create(serializer) 

+

130 headers = self.get_success_headers(serializer.data) 

+

131 return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) 

+

132 

+

133 def perform_create(self, serializer): 

+

134 serializer.save(owner=self.request.user) 

+

135 

+

136 def get_queryset(self): 

+

137 qs = Offer.objects.none() 

+

138 

+

139 if self.request.user: 

+

140 qs = Offer.objects.filter( 

+

141 Q(owner=self.request.user) | Q(recipient=self.request.user) 

+

142 ).distinct() 

+

143 

+

144 # filtering by status (if provided) 

+

145 status = self.request.query_params.get("status", None) 

+

146 if status is not None: 

+

147 qs = qs.filter(status=status) 

+

148 return qs 

+

149 

+

150 

+

151class OfferDetail( 

+

152 mixins.RetrieveModelMixin, 

+

153 mixins.DestroyModelMixin, 

+

154 generics.GenericAPIView, 

+

155): 

+

156 permission_classes = [IsAuthenticatedOrReadOnly] 

+

157 queryset = Offer.objects.all() 

+

158 serializer_class = OfferSerializer 

+

159 

+

160 def get(self, request, *args, **kwargs): 

+

161 return self.retrieve(request, *args, **kwargs) 

+

162 

+

163 def put(self, request, *args, **kwargs): 

+

164 id = kwargs.get('pk') 

+

165 offer = Offer.objects.get(pk=id) 

+

166 

+

167 try: 

+

168 offer.status = request.data.get('status') 

+

169 

+

170 # Add the sender of the offer to the recipients list of athletes, and add the recipient to the senders coach list 

+

171 if offer.status == 'a': 

+

172 if not offer.recipient.isCoach: 

+

173 return Response({'error': 'Recipient is not a coach'}, status=400) 

+

174 

+

175 offer.recipient.athletes.add(offer.owner) 

+

176 offer.recipient.save() 

+

177 

+

178 # Delete other pending offers from the same sender to the same recipient 

+

179 Offer.objects.filter(owner=offer.owner, recipient=offer.recipient, status='p').delete() 

+

180 

+

181 except Exception as e: 

+

182 return Response({f'error: {e}'}, status=400) 

+

183 

+

184 partial = kwargs.pop('partial', False) 

+

185 serializer = self.get_serializer(offer, data=request.data, partial=partial) 

+

186 serializer.is_valid(raise_exception=True) 

+

187 serializer.save() 

+

188 

+

189 return Response(serializer.data) 

+

190 

+

191 def patch(self, request, *args, **kwargs): 

+

192 return self.partial_update(request, *args, **kwargs) 

+

193 

+

194 def delete(self, request, *args, **kwargs): 

+

195 return self.destroy(request, *args, **kwargs) 

+

196 

+

197 

+

198class AthleteFileList( 

+

199 mixins.ListModelMixin, 

+

200 mixins.CreateModelMixin, 

+

201 CreateListModelMixin, 

+

202 generics.GenericAPIView, 

+

203): 

+

204 queryset = AthleteFile.objects.all() 

+

205 serializer_class = AthleteFileSerializer 

+

206 permission_classes = [permissions.IsAuthenticated & (IsAthlete | IsCoach)] 

+

207 parser_classes = [MultiPartParser, FormParser] 

+

208 

+

209 def get(self, request, *args, **kwargs): 

+

210 return self.list(request, *args, **kwargs) 

+

211 

+

212 def post(self, request, *args, **kwargs): 

+

213 return self.create(request, *args, **kwargs) 

+

214 

+

215 def perform_create(self, serializer): 

+

216 serializer.save(owner=self.request.user) 

+

217 

+

218 def get_queryset(self): 

+

219 user = self.request.user 

+

220 if user.isCoach: 

+

221 # Return files for athletes coached by this user 

+

222 return AthleteFile.objects.filter(athlete__coach=user) 

+

223 else: 

+

224 # Return files for the current athlete 

+

225 return AthleteFile.objects.filter(athlete=user) 

+

226 

+

227 

+

228class AthleteFileDetail( 

+

229 mixins.RetrieveModelMixin, 

+

230 mixins.UpdateModelMixin, 

+

231 mixins.DestroyModelMixin, 

+

232 generics.GenericAPIView, 

+

233): 

+

234 queryset = AthleteFile.objects.all() 

+

235 serializer_class = AthleteFileSerializer 

+

236 permission_classes = [permissions.IsAuthenticated & (IsAthlete | IsOwner)] 

+

237 

+

238 def get(self, request, *args, **kwargs): 

+

239 return self.retrieve(request, *args, **kwargs) 

+

240 

+

241 def delete(self, request, *args, **kwargs): 

+

242 return self.destroy(request, *args, **kwargs) 

+
+ + + diff --git a/backend/htmlcov/z_fff39e3ca0aaeed4_0001_initial_py.html b/backend/htmlcov/z_fff39e3ca0aaeed4_0001_initial_py.html new file mode 100644 index 0000000..1221d2e --- /dev/null +++ b/backend/htmlcov/z_fff39e3ca0aaeed4_0001_initial_py.html @@ -0,0 +1,164 @@ + + + + + Coverage for users\migrations\0001_initial.py: 100% + + + + + +
+
+

+ Coverage for users\migrations\0001_initial.py: + 100% +

+ +

+ 11 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1# Generated by Django 4.0.8 on 2024-07-29 11:55 

+

2 

+

3from django.conf import settings 

+

4import django.contrib.auth.models 

+

5import django.contrib.auth.validators 

+

6from django.db import migrations, models 

+

7import django.db.models.deletion 

+

8import django.utils.timezone 

+

9import users.models 

+

10 

+

11 

+

12class Migration(migrations.Migration): 

+

13 

+

14 initial = True 

+

15 

+

16 dependencies = [ 

+

17 ('auth', '0012_alter_user_first_name_max_length'), 

+

18 ] 

+

19 

+

20 operations = [ 

+

21 migrations.CreateModel( 

+

22 name='User', 

+

23 fields=[ 

+

24 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 

+

25 ('password', models.CharField(max_length=128, verbose_name='password')), 

+

26 ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 

+

27 ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 

+

28 ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 

+

29 ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), 

+

30 ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 

+

31 ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 

+

32 ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 

+

33 ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 

+

34 ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 

+

35 ('coach', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='athletes', to=settings.AUTH_USER_MODEL)), 

+

36 ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), 

+

37 ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), 

+

38 ], 

+

39 options={ 

+

40 'verbose_name': 'user', 

+

41 'verbose_name_plural': 'users', 

+

42 'abstract': False, 

+

43 }, 

+

44 managers=[ 

+

45 ('objects', django.contrib.auth.models.UserManager()), 

+

46 ], 

+

47 ), 

+

48 migrations.CreateModel( 

+

49 name='Offer', 

+

50 fields=[ 

+

51 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 

+

52 ('status', models.CharField(choices=[('a', 'Accepted'), ('p', 'Pending'), ('d', 'Declined')], default='p', max_length=8)), 

+

53 ('timestamp', models.DateTimeField(auto_now_add=True)), 

+

54 ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_offers', to=settings.AUTH_USER_MODEL)), 

+

55 ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_offers', to=settings.AUTH_USER_MODEL)), 

+

56 ], 

+

57 ), 

+

58 migrations.CreateModel( 

+

59 name='AthleteFile', 

+

60 fields=[ 

+

61 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 

+

62 ('file', models.FileField(upload_to=users.models.athlete_directory_path)), 

+

63 ('athlete', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='coach_files', to=settings.AUTH_USER_MODEL)), 

+

64 ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='athlete_files', to=settings.AUTH_USER_MODEL)), 

+

65 ], 

+

66 ), 

+

67 ] 

+
+ + + diff --git a/backend/htmlcov/z_fff39e3ca0aaeed4_0002_user_iscoach_py.html b/backend/htmlcov/z_fff39e3ca0aaeed4_0002_user_iscoach_py.html new file mode 100644 index 0000000..c50e071 --- /dev/null +++ b/backend/htmlcov/z_fff39e3ca0aaeed4_0002_user_iscoach_py.html @@ -0,0 +1,115 @@ + + + + + Coverage for users\migrations\0002_user_iscoach.py: 100% + + + + + +
+
+

+ Coverage for users\migrations\0002_user_iscoach.py: + 100% +

+ +

+ 4 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1# Generated by Django 4.0.8 on 2024-08-19 10:10 

+

2 

+

3from django.db import migrations, models 

+

4 

+

5 

+

6class Migration(migrations.Migration): 

+

7 

+

8 dependencies = [ 

+

9 ('users', '0001_initial'), 

+

10 ] 

+

11 

+

12 operations = [ 

+

13 migrations.AddField( 

+

14 model_name='user', 

+

15 name='isCoach', 

+

16 field=models.BooleanField(default=False), 

+

17 ), 

+

18 ] 

+
+ + + diff --git a/backend/htmlcov/z_fff39e3ca0aaeed4_0003_user_specialism_py.html b/backend/htmlcov/z_fff39e3ca0aaeed4_0003_user_specialism_py.html new file mode 100644 index 0000000..0dc90dd --- /dev/null +++ b/backend/htmlcov/z_fff39e3ca0aaeed4_0003_user_specialism_py.html @@ -0,0 +1,115 @@ + + + + + Coverage for users\migrations\0003_user_specialism.py: 100% + + + + + +
+
+

+ Coverage for users\migrations\0003_user_specialism.py: + 100% +

+ +

+ 4 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+

1# Generated by Django 4.0.8 on 2024-08-19 14:22 

+

2 

+

3from django.db import migrations, models 

+

4 

+

5 

+

6class Migration(migrations.Migration): 

+

7 

+

8 dependencies = [ 

+

9 ('users', '0002_user_iscoach'), 

+

10 ] 

+

11 

+

12 operations = [ 

+

13 migrations.AddField( 

+

14 model_name='user', 

+

15 name='specialism', 

+

16 field=models.CharField(default='', max_length=1000), 

+

17 ), 

+

18 ] 

+
+ + + diff --git a/backend/htmlcov/z_fff39e3ca0aaeed4___init___py.html b/backend/htmlcov/z_fff39e3ca0aaeed4___init___py.html new file mode 100644 index 0000000..5678591 --- /dev/null +++ b/backend/htmlcov/z_fff39e3ca0aaeed4___init___py.html @@ -0,0 +1,97 @@ + + + + + Coverage for users\migrations\__init__.py: 100% + + + + + +
+
+

+ Coverage for users\migrations\__init__.py: + 100% +

+ +

+ 0 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.7.1, + created at 2025-04-03 19:25 +0200 +

+ +
+
+
+
+ + +