diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000..63c7aa7e89 Binary files /dev/null and b/.coverage differ diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..624291b553 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,13 @@ +[run] +omit = + */tests.py + */tests/* + */test_*.py + */tests_*.py + */migrations/* + */apps.py + */__init__.py + */admin.py + */asgi.py + */wsgi.py + */settings.py \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..038e96f73a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +node_modules/ +vendor/ +__pycache__/ +*.pyc + +.git/ +.gitignore +.vscode/ +.idea/ +*.swp + +dist/ +build/ +*.log + +.env +.env.local +*.pem +*.key +secrets/ +.npmrc +.pypirc +kubeconfig + +cov_html +flake8-report diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml new file mode 100644 index 0000000000..4d53895f6b --- /dev/null +++ b/.github/workflows/django.yml @@ -0,0 +1,106 @@ +name: OC Lettings site CI/CD + +on: + push: + branches: + - '**' + pull_request: + branches: + - '**' + +jobs: + ci: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + env: + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run Lint + run: | + flake8 + + - name: Upload flake8 report + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: flake8-report + path: flake8-report/ + if-no-files-found: warn + retention-days: 5 + + - name: Run Tests + run: | + pytest + + - name: Upload coverage report + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: cov_html + path: cov_html/ + if-no-files-found: warn + retention-days: 5 + + deploy: + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + needs: ci + environment: production + concurrency: production + + steps: + - uses: actions/checkout@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ secrets.DOCKER_USERNAME }}/oc_lettings_site + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v4 + with: + no-cache: true + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Install Heroku CLI + run: | + curl https://cli-assets.heroku.com/install.sh | sh + + - name: Deploy to Heroku + uses: akhileshns/heroku-deploy@v3.15.15 + with: + heroku_api_key: ${{secrets.HEROKU_API_KEY}} + heroku_app_name: "orange-county-lettings" + heroku_email: ${{ secrets.HEROKU_USER_EMAIL }} + usedocker: true + docker_build_args: | + SENTRY_KEY + env: + SENTRY_KEY: ${{ secrets.SENTRY_KEY }} diff --git a/.gitignore b/.gitignore index b4405ebab4..6ed860f88d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,18 @@ +# Temp files **/__pycache__ *.pyc + +# Virtual environment venv +env +env-docs + +# Idea +.idea + +# Database +backup.sqlite3 +oc-lettings-site_old.sqlite3 + +# .env file +.env \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..12180598fc --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version, and other tools you might need +build: + os: ubuntu-24.04 + tools: + python: "3.10" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# Optionally, but recommended, +# declare the Python requirements required to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: ./requirements-docs.txt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..ce0e449da9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.10-slim + +WORKDIR /app + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV DEBUG=$DEBUG + +COPY ./requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN python manage.py collectstatic --noinput + +EXPOSE 8000 + +CMD gunicorn oc_lettings_site.wsgi:application --bind 0.0.0.0:${PORT:-8000} diff --git a/README.md b/README.md index c8547803f7..06e01a0d0e 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,262 @@ -## Résumé +# Django App - OpenClassrooms Project 13 +**Scale a Django application using a modular architecture** -Site web d'Orange County Lettings +--- -## Développement local +## DESCRIPTION -### Prérequis +This project was completed as part of the "Python Developer" path at OpenClassrooms. -- Compte GitHub avec accès en lecture à ce repository -- Git CLI -- SQLite3 CLI -- Interpréteur Python, version 3.6 ou supérieure +The goal was to scale a Django application using a modular architecture : -Dans le reste de la documentation sur le développement local, il est supposé que la commande `python` de votre OS shell exécute l'interpréteur Python ci-dessus (à moins qu'un environnement virtuel ne soit activé). +- Redesign of the modular architecture in the GitHub repository; +- Reduction of various technical debts on the project; +- Addition and deployment of a CI/CD pipeline; +- Application monitoring and error tracking via Sentry; +- Creation of the application's technical documentation using Read The Docs and Sphinx. -### macOS / Linux +The application must: -#### Cloner le repository +- allow the users to view available rentals and all the registered profiles. -- `cd /path/to/put/project/in` -- `git clone https://github.com/OpenClassrooms-Student-Center/Python-OC-Lettings-FR.git` +--- -#### Créer l'environnement virtuel +## PROJECT STRUCTURE +

+ +

-- `cd /path/to/Python-OC-Lettings-FR` -- `python -m venv venv` -- `apt-get install python3-venv` (Si l'étape précédente comporte des erreurs avec un paquet non trouvé sur Ubuntu) -- Activer l'environnement `source venv/bin/activate` -- Confirmer que la commande `python` exécute l'interpréteur Python dans l'environnement virtuel -`which python` -- Confirmer que la version de l'interpréteur Python est la version 3.6 ou supérieure `python --version` -- Confirmer que la commande `pip` exécute l'exécutable pip dans l'environnement virtuel, `which pip` -- Pour désactiver l'environnement, `deactivate` +--- -#### Exécuter le site +## INSTALLATION -- `cd /path/to/Python-OC-Lettings-FR` -- `source venv/bin/activate` -- `pip install --requirement requirements.txt` -- `python manage.py runserver` -- Aller sur `http://localhost:8000` dans un navigateur. -- Confirmer que le site fonctionne et qu'il est possible de naviguer (vous devriez voir plusieurs profils et locations). +- ### Clone the repository : -#### Linting +``` +git clone https://github.com/Tit-Co/OpenClassrooms_Project_13.git +``` -- `cd /path/to/Python-OC-Lettings-FR` -- `source venv/bin/activate` -- `flake8` +- ### Navigate into the project directory : + `cd OpenClassrooms_Project_13` -#### Tests unitaires +- ### Create a virtual environment and dependencies : -- `cd /path/to/Python-OC-Lettings-FR` -- `source venv/bin/activate` -- `pytest` +1. #### With [uv](https://docs.astral.sh/uv/) -#### Base de données + `uv` is an environment and dependencies manager. + + - #### Install environment and dependencies + + `uv sync` -- `cd /path/to/Python-OC-Lettings-FR` -- Ouvrir une session shell `sqlite3` -- Se connecter à la base de données `.open oc-lettings-site.sqlite3` -- Afficher les tables dans la base de données `.tables` -- Afficher les colonnes dans le tableau des profils, `pragma table_info(Python-OC-Lettings-FR_profile);` -- Lancer une requête sur la table des profils, `select user_id, favorite_city from - Python-OC-Lettings-FR_profile where favorite_city like 'B%';` -- `.quit` pour quitter +2. #### With pip -#### Panel d'administration + - #### Install the virtual env : -- Aller sur `http://localhost:8000/admin` -- Connectez-vous avec l'utilisateur `admin`, mot de passe `Abc1234!` + `python -m venv env` -### Windows + - #### Activate the virtual env : + `source env/bin/activate` in Git Bash on Windows or on macOS / Linux + Or + `env\Scripts\activate` on Windows -Utilisation de PowerShell, comme ci-dessus sauf : +3. #### With [Poetry](https://python-poetry.org/docs/) -- Pour activer l'environnement virtuel, `.\venv\Scripts\Activate.ps1` -- Remplacer `which ` par `(Get-Command ).Path` + `Poetry` is a tool for dependency management and packaging in Python. + + - #### Install the virtual env : + `py -3.10 -m venv env` + + - #### Activate the virtual env : + `poetry env activate` + +- ### Install dependencies + 1. #### With [uv](https://docs.astral.sh/uv/) + `uv sync` or `uv pip install -r requirements.txt` or `uv add -r requirements.txt` + + 2. #### With pip + `pip install -r requirements.txt` + + 3. #### With [Poetry](https://python-poetry.org/docs/) + `poetry install` + + (NB : Poetry and uv will read the `pyproject.toml` file to know which dependencies to install) + +--- + +## USAGE + +### Launching server +- Open a terminal +- Go to project folder - example : `cd oc_lettings_site` +- Activate the virtual environment as described previously +- Create environment variables (to avoid to add raw Sentry key into the code) + - With Power Shell : + ``` + $env:SENTRY_KEY = "my_key" + ``` + - With Git Bash : + ``` + export SENTRY_KEY = "my_key" + ``` +- Launch the local server by typing the command : + - `python manage.py runserver` + +### Launching the APP +- With local server, open a web browser and type the urls : + - [http://127.0.0.1:8000/](http://127.0.0.1:8000/) + - [http://127.0.0.1:8000/admin](http://127.0.0.1:8000/admin) + - for the admin panel (username: ```admin```, password: given in the project technical specifications) + +- With web server (after deployment), open a web browser and type the url : + - your Heroku app url given in the Heroku dashboard, for example the url below : +[Heroku app](https://orange-county-lettings-7b4c4811f25f.herokuapp.com/) + +--- + +## APP EXAMPLES + +Here are some examples of the application execution. + +- Home page +

+ +

+ +- Lettings index +

+ +

+ +- Letting details +

+ +

+ +- Profiles index +

+ +

+ +- Profile details +

+ +

+ +--- + +## PEP 8 CONVENTIONS + +- ### Flake 8 report +

+ +

+ +- **Type the line below in the terminal to generate another report with [flake8-html](https://pypi.org/project/flake8-html/) tool :** + + ` flake8` + - The app code has a setup.cfg file that specify Flake 8 options as below : + ``` + format = html + htmldir = flake8-report + max-line-length = 99 + exclude = **/migrations/*,env,cov_html + ``` + +--- + +## TESTS COVERAGE WITH PYTEST + +- ### Coverage report +

+ + +

+ +- **Type the line below in the terminal to generate another coverage report with pytest** + + `pytest` + - The app code has a setup.cfg file that specify Pytest options as below : + ``` + python_files = tests*.py + addopts = -v --cov=lettings --cov=profiles --cov=oc_lettings_site --cov-report=html:cov_html + ``` +--- + +## DEPLOYMENT +- ### Docker + - The application is containerized using Docker. + - The Dockerfile located at the project root defines the build process: + - install Python dependencies + - copy the Django project + - collect static files + - start the application with Gunicorn + + - The container can be executed locally for testing purposes. + +- ### CI/CD Pipeline + - The CI/CD pipeline is defined in .github/workflows/. + + - Continuous Integration + + - The CI workflow runs on every branch push and includes: + - repository checkout + - dependency installation + - linting + - test execution with pytest + - coverage and quality report generation + - Continuous Deployment + + - Deployment is only triggered on the master branch after successful CI validation. + + - The deployment workflow: + - builds the Docker image + - pushes the image to Heroku Container Registry + - releases the application on Heroku + +- ### Heroku Deployment + - The application is deployed on Heroku using the container stack. + + - Required environment variables must be configured in the Heroku dashboard: + + - `DEBUG` + - `DJANGO_ALLOWED_HOSTS` + - `SECRET_KEY` + +- ### Monitoring + - Application monitoring and exception tracking are handled using Sentry. + + - To enable monitoring: + - Create a Sentry account + - Generate a project key + - Add the key as a GitHub repository secret: + - `SENTRY_KEY` + + - The secret is injected into the deployment workflow through GitHub Actions. + +## DOCUMENTATION +- ### ReadTheDocs documentation : + - A ReadTheDocs documentation for technical specifications is linked to the repository + - The documentation includes : + - a project description + - project installation instructions + - a quick start guide + - the technologies and programming languages to be used + - a description of the database structure and data models + - a description of the programming interfaces + - a user guide (with use cases) + - application deployment and management procedures + +--- + +![Python](https://img.shields.io/badge/python-3.10-blue.svg) +![License](https://img.shields.io/badge/license-MIT-green.svg) +![Coverage](https://img.shields.io/badge/coverage-100%25-red) +[![Documentation Status](https://readthedocs.org/projects/tit-co-oc-lettings-documentation/badge/?version=latest)](https://tit-co-oc-lettings-documentation.readthedocs.io/en/latest/) + +--- + +## AUTHOR +**Name**: Nicolas MARIE +**Track**: Python Developer – OpenClassrooms +**Project 13 – Scale a Django application using a modular architecture – May 2026** diff --git a/config.py b/config.py new file mode 100644 index 0000000000..097854e7fc --- /dev/null +++ b/config.py @@ -0,0 +1,11 @@ +""" +Config module for environment variables +""" +import os + +from dotenv import load_dotenv + + +load_dotenv() + +SENTRY_KEY = os.getenv("SENTRY_KEY") diff --git a/cov_html/.gitignore b/cov_html/.gitignore new file mode 100644 index 0000000000..23006f1f32 --- /dev/null +++ b/cov_html/.gitignore @@ -0,0 +1 @@ +# Created by coverage.py diff --git a/cov_html/class_index.html b/cov_html/class_index.html new file mode 100644 index 0000000000..fac060a788 --- /dev/null +++ b/cov_html/class_index.html @@ -0,0 +1,231 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 100% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.14.0, + created at 2026-05-28 09:03 +0200 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fileclass statementsmissingexcluded coverage
lettings \ models.pyAddress 100 100%
lettings \ models.pyAddress.Meta 000 100%
lettings \ models.pyLetting 100 100%
lettings \ models.py(no class) 1700 100%
lettings \ urls.py(no class) 400 100%
lettings \ views.py(no class) 2800 100%
oc_lettings_site \ urls.py(no class) 400 100%
oc_lettings_site \ views.py(no class) 1200 100%
profiles \ models.pyProfile 100 100%
profiles \ models.py(no class) 600 100%
profiles \ urls.py(no class) 400 100%
profiles \ views.py(no class) 2800 100%
Total  10600 100%
+

+ No items found using the specified filter. +

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

Coverage report: + 100% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.14.0, + created at 2026-05-28 09:03 +0200 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Filefunction statementsmissingexcluded coverage
lettings \ models.pyAddress.__str__ 100 100%
lettings \ models.pyLetting.__str__ 100 100%
lettings \ models.py(no function) 1700 100%
lettings \ urls.py(no function) 400 100%
lettings \ views.pyindex 900 100%
lettings \ views.pyletting 1300 100%
lettings \ views.py(no function) 600 100%
oc_lettings_site \ urls.py(no function) 400 100%
oc_lettings_site \ views.pyindex 800 100%
oc_lettings_site \ views.py(no function) 400 100%
profiles \ models.pyProfile.__str__ 100 100%
profiles \ models.py(no function) 600 100%
profiles \ urls.py(no function) 400 100%
profiles \ views.pyindex 900 100%
profiles \ views.pyprofile 1300 100%
profiles \ views.py(no function) 600 100%
Total  10600 100%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/cov_html/index.html b/cov_html/index.html new file mode 100644 index 0000000000..6b45b2611a --- /dev/null +++ b/cov_html/index.html @@ -0,0 +1,180 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 100% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.14.0, + created at 2026-05-28 09:03 +0200 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
File statementsmissingexcluded coverage
lettings \ models.py 1900 100%
lettings \ urls.py 400 100%
lettings \ views.py 2800 100%
oc_lettings_site \ urls.py 400 100%
oc_lettings_site \ views.py 1200 100%
profiles \ models.py 700 100%
profiles \ urls.py 400 100%
profiles \ views.py 2800 100%
Total 10600 100%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/cov_html/keybd_closed_cb_900cfef5.png b/cov_html/keybd_closed_cb_900cfef5.png new file mode 100644 index 0000000000..ba119c47df Binary files /dev/null and b/cov_html/keybd_closed_cb_900cfef5.png differ diff --git a/cov_html/status.json b/cov_html/status.json new file mode 100644 index 0000000000..603ab9a44f --- /dev/null +++ b/cov_html/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.14.0","globals":"5d68ca9844fe654e0fbee8d2e59249f4","files":{"z_a9b1b018a4ee155b_models_py":{"hash":"a5537957613848ba266b88176a8e0ab7","index":{"url":"z_a9b1b018a4ee155b_models_py.html","file":"lettings\\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_a9b1b018a4ee155b_urls_py":{"hash":"926e542cc4f7b882a38e94b89c64788b","index":{"url":"z_a9b1b018a4ee155b_urls_py.html","file":"lettings\\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_a9b1b018a4ee155b_views_py":{"hash":"e9bb9c2749d3a2cd8f649feb1aad8678","index":{"url":"z_a9b1b018a4ee155b_views_py.html","file":"lettings\\views.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_d8907effad88fb1e_urls_py":{"hash":"f5c03788ee38dd1b9a052320b7c8e410","index":{"url":"z_d8907effad88fb1e_urls_py.html","file":"oc_lettings_site\\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_d8907effad88fb1e_views_py":{"hash":"74d323ea494235787d9d16f139c9f823","index":{"url":"z_d8907effad88fb1e_views_py.html","file":"oc_lettings_site\\views.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":12,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_b4872cbce420d762_models_py":{"hash":"2de4f9efdcb0811adaf7246ef9369bbe","index":{"url":"z_b4872cbce420d762_models_py.html","file":"profiles\\models.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_b4872cbce420d762_urls_py":{"hash":"b9923831fb1c54710d4be641bd654bc2","index":{"url":"z_b4872cbce420d762_urls_py.html","file":"profiles\\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_b4872cbce420d762_views_py":{"hash":"984fe4add5c47a729d8520f5af13a56e","index":{"url":"z_b4872cbce420d762_views_py.html","file":"profiles\\views.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}}}}} \ No newline at end of file diff --git a/cov_html/style_cb_5c747636.css b/cov_html/style_cb_5c747636.css new file mode 100644 index 0000000000..5e304ce5f6 --- /dev/null +++ b/cov_html/style_cb_5c747636.css @@ -0,0 +1,389 @@ +@charset "UTF-8"; +/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ +/* For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt */ +/* Don't edit this .css file. Edit the .scss file instead! */ +html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } + +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { body { color: #eee; } } + +html > body { font-size: 16px; } + +a:active, a:focus { outline: 2px dashed #007acc; } + +p { font-size: .875em; line-height: 1.4em; } + +table { border-collapse: collapse; } + +td { vertical-align: top; } + +table tr.hidden { display: none !important; } + +p#no_rows { display: none; font-size: 1.15em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +a.nav { text-decoration: none; color: inherit; } + +a.nav:hover { text-decoration: underline; color: inherit; } + +.hidden { display: none; } + +header { background: #f8f8f8; width: 100%; z-index: 2; border-bottom: 1px solid #ccc; } + +@media (prefers-color-scheme: dark) { header { background: black; } } + +@media (prefers-color-scheme: dark) { header { border-color: #333; } } + +header .content { padding: 1rem 3.5rem; } + +header h2 { margin-top: .5em; font-size: 1em; } + +header h2 a.button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header h2 a.button { background: #333; } } + +@media (prefers-color-scheme: dark) { header h2 a.button { border-color: #444; } } + +header h2 a.button.current { border: 2px solid; background: #fff; border-color: #999; cursor: default; } + +@media (prefers-color-scheme: dark) { header h2 a.button.current { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { header h2 a.button.current { border-color: #777; } } + +header p.text { margin: .5em 0 -.5em; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { header p.text { color: #aaa; } } + +header.sticky { position: fixed; left: 0; right: 0; height: 2.5em; } + +header.sticky .text { display: none; } + +header.sticky h1, header.sticky h2 { font-size: 1em; margin-top: 0; display: inline-block; } + +header.sticky .content { padding: 0.5rem 3.5rem; } + +header.sticky .content p { font-size: 1em; } + +header.sticky ~ #source { padding-top: 6.5em; } + +main { position: relative; z-index: 1; } + +footer { margin: 1rem 3.5rem; } + +footer .content { padding: 0; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { footer .content { color: #aaa; } } + +#index { margin: 1rem 0 0 3.5rem; } + +h1 { font-size: 1.25em; display: inline-block; } + +#filter_container { float: right; margin: 0 2em 0 0; line-height: 1.66em; } + +#filter_container #filter { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { #filter_container #filter { border-color: #444; } } + +@media (prefers-color-scheme: dark) { #filter_container #filter { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #filter_container #filter { color: #eee; } } + +#filter_container #filter:focus { border-color: #007acc; } + +#filter_container :disabled ~ label { color: #ccc; } + +@media (prefers-color-scheme: dark) { #filter_container :disabled ~ label { color: #444; } } + +#filter_container label { font-size: .875em; color: #666; } + +@media (prefers-color-scheme: dark) { #filter_container label { color: #aaa; } } + +header button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header button { background: #333; } } + +@media (prefers-color-scheme: dark) { header button { border-color: #444; } } + +header button:active, header button:focus { outline: 2px dashed #007acc; } + +header button.run { background: #eeffee; } + +@media (prefers-color-scheme: dark) { header button.run { background: #373d29; } } + +header button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.run.show_run { background: #373d29; } } + +header button.mis { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { header button.mis { background: #4b1818; } } + +header button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.mis.show_mis { background: #4b1818; } } + +header button.exc { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { header button.exc { background: #333; } } + +header button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.exc.show_exc { background: #333; } } + +header button.par { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { header button.par { background: #650; } } + +header button.par.show_par { background: #ffa; border: 2px solid #bbbb00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.par.show_par { background: #650; } } + +#help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } + +#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } + +#help_panel_wrapper { float: right; position: relative; } + +#keyboard_icon { margin: 5px; } + +#help_panel_state { display: none; } + +#help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; color: #333; } + +#help_panel .keyhelp p { margin-top: .75em; } + +#help_panel .legend { font-style: italic; margin-bottom: 1em; } + +.indexfile #help_panel { width: 25em; } + +.pyfile #help_panel { width: 18em; } + +#help_panel_state:checked ~ #help_panel { display: block; } + +kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; border-radius: 3px; } + +#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } + +#source p { position: relative; white-space: pre; } + +#source p * { box-sizing: border-box; } + +#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; user-select: none; } + +@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } + +#source p .n.highlight { background: #ffdd00; } + +#source p .n a { scroll-margin-top: 6em; text-decoration: none; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } + +#source p .n a:hover { text-decoration: underline; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } + +#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } + +@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } + +#source p .t:hover { background: #f2f2f2; } + +@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } + +#source p .t:hover ~ .r .annotate.long { display: block; } + +#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } + +@media (prefers-color-scheme: dark) { #source p .t .com { color: #6a9955; } } + +#source p .t .key { font-weight: bold; line-height: 1px; } + +#source p .t .str, #source p .t .fst { color: #0451a5; } + +@media (prefers-color-scheme: dark) { #source p .t .str, #source p .t .fst { color: #9cdcfe; } } + +#source p.mis .t { border-left: 0.2em solid #ff0000; } + +#source p.mis.show_mis .t { background: #fdd; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } + +#source p.mis.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } + +#source p.mis.mis2 .t { border-left: 0.2em dotted #ff0000; } + +#source p.mis.mis2.show_mis .t { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { #source p.mis.mis2.show_mis .t { background: #351b1b; } } + +#source p.mis.mis2.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.mis2.show_mis .t:hover { background: #532323; } } + +#source p.run .t { border-left: 0.2em solid #00dd00; } + +#source p.run.show_run .t { background: #dfd; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } + +#source p.run.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } + +#source p.run.run2 .t { border-left: 0.2em dotted #00dd00; } + +#source p.run.run2.show_run .t { background: #eeffee; } + +@media (prefers-color-scheme: dark) { #source p.run.run2.show_run .t { background: #2b2e24; } } + +#source p.run.run2.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.run2.show_run .t:hover { background: #404633; } } + +#source p.exc .t { border-left: 0.2em solid #808080; } + +#source p.exc.show_exc .t { background: #eee; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } + +#source p.exc.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } + +#source p.exc.exc2 .t { border-left: 0.2em dotted #808080; } + +#source p.exc.exc2.show_exc .t { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { #source p.exc.exc2.show_exc .t { background: #292929; } } + +#source p.exc.exc2.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.exc2.show_exc .t:hover { background: #3c3c3c; } } + +#source p.par .t { border-left: 0.2em solid #bbbb00; } + +#source p.par.show_par .t { background: #ffa; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } + +#source p.par.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } + +#source p.par.par2 .t { border-left: 0.2em dotted #bbbb00; } + +#source p.par.par2.show_par .t { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { #source p.par.par2.show_par .t { background: #423a0f; } } + +#source p.par.par2.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.par2.show_par .t:hover { background: #6d5d0c; } } + +#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } + +@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } + +#source p .annotate.short:hover ~ .long { display: block; } + +#source p .annotate.long { width: 30em; right: 2.5em; } + +#source p input { display: none; } + +#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } + +#source p input ~ .r label.ctx::before { content: "▶ "; } + +#source p input ~ .r label.ctx:hover { background: #e8f4ff; color: #666; } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } + +#source p input:checked ~ .r label.ctx { background: #d0e8ff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } + +#source p input:checked ~ .r label.ctx::before { content: "▼ "; } + +#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } + +#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } + +@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } + +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; } + +@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } + +#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } + +#index table.index { margin-left: -.5em; } + +#index td, #index th { text-align: right; vertical-align: baseline; padding: .25em .5em; border-bottom: 1px solid #eee; } + +@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } + +#index td.name, #index th.name { text-align: left; width: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; min-width: 15em; } + +#index td.left, #index th.left { text-align: left; } + +#index td.spacer, #index th.spacer { border: none; padding: 0; } + +#index td.spacer:hover, #index th.spacer:hover { background: inherit; } + +#index th { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-style: italic; color: #333; border-color: #ccc; cursor: pointer; } + +@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } + +@media (prefers-color-scheme: dark) { #index th { border-color: #444; } } + +#index th:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } + +#index th .arrows { color: #666; font-size: 85%; font-family: sans-serif; font-style: normal; pointer-events: none; } + +#index th[aria-sort="ascending"], #index th[aria-sort="descending"] { white-space: nowrap; background: #eee; padding-left: .5em; } + +@media (prefers-color-scheme: dark) { #index th[aria-sort="ascending"], #index th[aria-sort="descending"] { background: #333; } } + +#index th[aria-sort="ascending"] .arrows::after { content: " ▲"; } + +#index th[aria-sort="descending"] .arrows::after { content: " ▼"; } + +#index tr.grouphead th { cursor: default; font-style: normal; border-color: #999; } + +@media (prefers-color-scheme: dark) { #index tr.grouphead th { border-color: #777; } } + +#index td.name { font-size: 1.15em; } + +#index td.name a { text-decoration: none; color: inherit; } + +#index td.name .no-noun { font-style: italic; } + +#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-bottom: none; } + +#index tr.region:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index tr.region:hover { background: #333; } } + +#index tr.region:hover td.name { text-decoration: underline; color: inherit; } + +#scroll_marker { position: fixed; z-index: 3; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } + +@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } + +#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } + +@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } diff --git a/cov_html/z_a9b1b018a4ee155b_models_py.html b/cov_html/z_a9b1b018a4ee155b_models_py.html new file mode 100644 index 0000000000..5448b1b187 --- /dev/null +++ b/cov_html/z_a9b1b018a4ee155b_models_py.html @@ -0,0 +1,155 @@ + + + + + Coverage for lettings\models.py: 100% + + + + + +
+
+

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

+ +

+ 19 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-23 08:21 +0200 +

+ +
+
+
+

1""" 

+

2Models module for lettings app 

+

3""" 

+

4from django.db import models 

+

5from django.core.validators import MaxValueValidator, MinLengthValidator 

+

6 

+

7 

+

8class Address(models.Model): 

+

9 """ 

+

10 Address model for Lettings 

+

11 Attributes: 

+

12 number (int): Letting number 

+

13 street (str): Street address 

+

14 city (str): City address 

+

15 state (str): State address 

+

16 zip_code (int): Zip code 

+

17 country_iso_code (int): Country code 

+

18 """ 

+

19 number = models.PositiveIntegerField(validators=[MaxValueValidator(9999)]) 

+

20 street = models.CharField(max_length=64) 

+

21 city = models.CharField(max_length=64) 

+

22 state = models.CharField(max_length=2, validators=[MinLengthValidator(2)]) 

+

23 zip_code = models.PositiveIntegerField(validators=[MaxValueValidator(99999)]) 

+

24 country_iso_code = models.CharField(max_length=3, validators=[MinLengthValidator(3)]) 

+

25 

+

26 class Meta: 

+

27 """ 

+

28 Meta class for Lettings to specify verbose names 

+

29 """ 

+

30 verbose_name = "Address" 

+

31 verbose_name_plural = "Addresses" 

+

32 

+

33 def __str__(self) -> str: 

+

34 """ 

+

35 string method for Lettings 

+

36 Returns: 

+

37 A f-string with number and street address 

+

38 """ 

+

39 return f'{self.number} {self.street}' 

+

40 

+

41 

+

42class Letting(models.Model): 

+

43 """ 

+

44 Letting model for Lettings 

+

45 Attributes: 

+

46 title (str): Letting title 

+

47 address (Address): Letting address 

+

48 """ 

+

49 title = models.CharField(max_length=256) 

+

50 address = models.OneToOneField(Address, on_delete=models.CASCADE) 

+

51 

+

52 def __str__(self) -> str: 

+

53 """ 

+

54 String method for Lettings 

+

55 Returns: 

+

56 A f-string with letting title 

+

57 """ 

+

58 return self.title 

+
+ + + diff --git a/cov_html/z_a9b1b018a4ee155b_urls_py.html b/cov_html/z_a9b1b018a4ee155b_urls_py.html new file mode 100644 index 0000000000..21e9fb25a8 --- /dev/null +++ b/cov_html/z_a9b1b018a4ee155b_urls_py.html @@ -0,0 +1,110 @@ + + + + + Coverage for lettings\urls.py: 100% + + + + + +
+
+

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

+ +

+ 4 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-23 08:21 +0200 +

+ +
+
+
+

1""" 

+

2URLs module for lettings app 

+

3""" 

+

4from django.urls import path 

+

5 

+

6from . import views 

+

7 

+

8app_name = 'lettings' 

+

9 

+

10urlpatterns = [ 

+

11 path('', views.index, name='index'), 

+

12 path('<int:letting_id>/', views.letting, name='letting'), 

+

13] 

+
+ + + diff --git a/cov_html/z_a9b1b018a4ee155b_views_py.html b/cov_html/z_a9b1b018a4ee155b_views_py.html new file mode 100644 index 0000000000..5400657090 --- /dev/null +++ b/cov_html/z_a9b1b018a4ee155b_views_py.html @@ -0,0 +1,198 @@ + + + + + Coverage for lettings\views.py: 100% + + + + + +
+
+

+ Coverage for lettings \ views.py: + 100% +

+ +

+ 28 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-28 08:59 +0200 +

+ +
+
+
+

1""" 

+

2Views module for lettings app 

+

3""" 

+

4from django.http import HttpRequest, HttpResponse 

+

5from django.shortcuts import render 

+

6 

+

7from .models import Letting 

+

8from monitoring import logger 

+

9 

+

10 

+

11# Aenean leo magna, vestibulum et tincidunt fermentum, consectetur quis velit. Sed non placerat 

+

12# massa. Integer est nunc, pulvinar a tempor et, bibendum id arcu. Vestibulum ante ipsum primis in 

+

13# faucibus orci luctus et ultrices posuere cubilia curae; Cras eget scelerisque 

+

14def index(request: HttpRequest) -> HttpResponse: 

+

15 """ 

+

16 View function for lettings index page 

+

17 Args: 

+

18 request (HttpRequest): Http Request object 

+

19 

+

20 Returns: 

+

21 An HTTP response with the list of lettings or an HTTP response with 500 error. 

+

22 """ 

+

23 try: 

+

24 lettings_list = Letting.objects.all() 

+

25 context = {'lettings_list': lettings_list} 

+

26 

+

27 logger.info(f"Going to lettings index page : {context=}, status = 200.") 

+

28 

+

29 return render(request=request, 

+

30 template_name='lettings/index.html', 

+

31 context=context, 

+

32 status=200) 

+

33 

+

34 except Exception as e: 

+

35 context = {"error": str(e)} 

+

36 

+

37 logger.error(f"Error 500 returned while reaching lettings index page : {context=}," 

+

38 f" status = 500.") 

+

39 

+

40 return render(request=request, 

+

41 template_name='oc_lettings_site/error_500.html', 

+

42 context=context, 

+

43 status=500) 

+

44 

+

45 

+

46# Cras ultricies dignissim purus, vitae hendrerit ex varius non. In accumsan porta nisl id 

+

47# eleifend. Praesent dignissim, odio eu consequat pretium, purus urna vulputate arcu, vitae 

+

48# efficitur lacus justo nec purus. Aenean finibus faucibus lectus at porta. Maecenas auctor, est ut 

+

49# luctus congue, dui enim mattis enim, ac condimentum velit libero in magna. Suspendisse potenti. 

+

50# In tempus a nisi sed laoreet. Suspendisse porta dui eget sem accumsan interdum. Ut quis urna 

+

51# pellentesque justo mattis ullamcorper ac non tellus. In tristique mauris eu velit fermentum, 

+

52# tempus pharetra est luctus. Vivamus consequat aliquam libero, eget bibendum lorem. Sed non dolor 

+

53# risus. Mauris condimentum auctor elementum. Donec quis nisi ligula. Integer vehicula tincidunt 

+

54# enim, ac lacinia augue pulvinar sit amet. 

+

55def letting(request: HttpRequest, letting_id: int) -> HttpResponse: 

+

56 """ 

+

57 View function for letting detail page 

+

58 Args: 

+

59 request (HttpRequest): Http Request object 

+

60 letting_id (int): letting id 

+

61 

+

62 Returns: 

+

63 An HTTP response with the letting detail or an HTTP response with 404 error if not found 

+

64 or an HTTP response with 500 error 

+

65 """ 

+

66 try: 

+

67 letting = Letting.objects.get(id=letting_id) 

+

68 

+

69 context = { 

+

70 'title': letting.title, 

+

71 'address': letting.address, 

+

72 } 

+

73 

+

74 logger.info(f"Going to lettings details page : {context=}, status = 200.") 

+

75 

+

76 return render(request=request, 

+

77 template_name='lettings/letting.html', 

+

78 context=context, 

+

79 status=200) 

+

80 

+

81 except Letting.DoesNotExist as e: 

+

82 context = {"type": "letting", "id": letting_id, "error": str(e)} 

+

83 

+

84 logger.warning(f"Error 404 returned while reaching letting n°{letting_id} : {context=}," 

+

85 f" status = 404.") 

+

86 

+

87 return render(request=request, 

+

88 template_name='oc_lettings_site/error_404.html', 

+

89 context=context, 

+

90 status=404) 

+

91 

+

92 except Exception as e: 

+

93 context = {"error": str(e)} 

+

94 

+

95 logger.error(f"Error 500 returned while reaching letting details page : {context=}," 

+

96 f" status = 500.") 

+

97 

+

98 return render(request=request, 

+

99 template_name='oc_lettings_site/error_500.html', 

+

100 context=context, 

+

101 status=500) 

+
+ + + diff --git a/cov_html/z_b4872cbce420d762_models_py.html b/cov_html/z_b4872cbce420d762_models_py.html new file mode 100644 index 0000000000..1adad65e95 --- /dev/null +++ b/cov_html/z_b4872cbce420d762_models_py.html @@ -0,0 +1,121 @@ + + + + + Coverage for profiles\models.py: 100% + + + + + +
+
+

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

+ +

+ 7 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-23 08:21 +0200 +

+ +
+
+
+

1""" 

+

2Models module for profiles app 

+

3""" 

+

4from django.db import models 

+

5from django.contrib.auth.models import User 

+

6 

+

7 

+

8class Profile(models.Model): 

+

9 """ 

+

10 Models class for profiles app 

+

11 Attributes: 

+

12 user (User): user 

+

13 favorite_city (str): The favorite city of the user 

+

14 """ 

+

15 user = models.OneToOneField(User, on_delete=models.CASCADE) 

+

16 favorite_city = models.CharField(max_length=64, blank=True) 

+

17 

+

18 def __str__(self) -> str: 

+

19 """ 

+

20 String method for profile model 

+

21 Returns: 

+

22 The user name of the profile 

+

23 """ 

+

24 return self.user.username 

+
+ + + diff --git a/cov_html/z_b4872cbce420d762_urls_py.html b/cov_html/z_b4872cbce420d762_urls_py.html new file mode 100644 index 0000000000..fca767902a --- /dev/null +++ b/cov_html/z_b4872cbce420d762_urls_py.html @@ -0,0 +1,110 @@ + + + + + Coverage for profiles\urls.py: 100% + + + + + +
+
+

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

+ +

+ 4 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-23 08:21 +0200 +

+ +
+
+
+

1""" 

+

2URLs module for profiles app 

+

3""" 

+

4from django.urls import path 

+

5 

+

6from . import views 

+

7 

+

8app_name = 'profiles' 

+

9 

+

10urlpatterns = [ 

+

11 path('', views.index, name='index'), 

+

12 path('<str:username>/', views.profile, name='profile'), 

+

13] 

+
+ + + diff --git a/cov_html/z_b4872cbce420d762_views_py.html b/cov_html/z_b4872cbce420d762_views_py.html new file mode 100644 index 0000000000..6fd0710be2 --- /dev/null +++ b/cov_html/z_b4872cbce420d762_views_py.html @@ -0,0 +1,186 @@ + + + + + Coverage for profiles\views.py: 100% + + + + + +
+
+

+ Coverage for profiles \ views.py: + 100% +

+ +

+ 28 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-28 08:59 +0200 +

+ +
+
+
+

1""" 

+

2Views module for profiles app 

+

3""" 

+

4from django.http import HttpRequest, HttpResponse 

+

5from django.shortcuts import render 

+

6from monitoring import logger 

+

7 

+

8from profiles.models import Profile 

+

9 

+

10 

+

11# Sed placerat quam in pulvinar commodo. Nullam laoreet consectetur ex, sed consequat libero 

+

12# pulvinar eget. Fusc faucibus, urna quis auctor pharetra, massa dolor cursus neque, quis dictum 

+

13# lacus d 

+

14def index(request: HttpRequest) -> HttpResponse: 

+

15 """ 

+

16 View function for profiles index page 

+

17 Args: 

+

18 request (HttpRequest): request object 

+

19 

+

20 Returns: 

+

21 An HTTP response with the list of profiles or HTTP response with 500 error. 

+

22 """ 

+

23 try: 

+

24 profiles_list = Profile.objects.all() 

+

25 context = {'profiles_list': profiles_list} 

+

26 

+

27 logger.info(f"Going to profiles index page : {context=}, status = 200.") 

+

28 

+

29 return render(request=request, 

+

30 template_name='profiles/index.html', 

+

31 context=context, 

+

32 status=200) 

+

33 

+

34 except Exception as e: 

+

35 context = {"error": str(e)} 

+

36 

+

37 logger.error(f"Error 500 returned while reaching profiles index page : {context=}" 

+

38 f", status = 500.") 

+

39 

+

40 return render(request=request, 

+

41 template_name='oc_lettings_site/error_500.html', 

+

42 context=context, 

+

43 status=500) 

+

44 

+

45 

+

46# Aliquam sed metus eget nisi tincidunt ornare accumsan eget lac 

+

47# laoreet neque quis, pellentesque dui. Nullam facilisis pharetra vulputate. Sed tincidunt, dolor 

+

48# id facilisis fringilla, eros leo tristique lacus, it. Nam aliquam dignissim congue. Pellentesque 

+

49# habitant morbi tristique senectus et netus et males 

+

50def profile(request: HttpRequest, username: str): 

+

51 """ 

+

52 View function for profile details page 

+

53 Args: 

+

54 request (HttpRequest): request object 

+

55 username (str): username 

+

56 

+

57 Returns: 

+

58 An HTTP response with the profile or HTTP response with 404 error if not found 

+

59 or an HTTP response with 500 error 

+

60 """ 

+

61 try: 

+

62 profile = Profile.objects.get(user__username=username) 

+

63 context = {'profile': profile} 

+

64 

+

65 logger.info(f"Going to profile details page : {context=}, status = 200.") 

+

66 

+

67 return render(request, template_name='profiles/profile.html', context=context, status=200) 

+

68 

+

69 except Profile.DoesNotExist as e: 

+

70 context = {"type": "profile", "name": username, "error": str(e)} 

+

71 

+

72 logger.warning(f"Error 404 returned while reaching profile {username} : {context=}," 

+

73 f" status = 404.") 

+

74 

+

75 return render(request=request, 

+

76 template_name='oc_lettings_site/error_404.html', 

+

77 context=context, 

+

78 status=404) 

+

79 

+

80 except Exception as e: 

+

81 context = {"error": str(e)} 

+

82 

+

83 logger.error(f"Error 500 returned while reaching profile details page : {context=}," 

+

84 f" status = 500.") 

+

85 

+

86 return render(request=request, 

+

87 template_name='oc_lettings_site/error_500.html', 

+

88 context=context, 

+

89 status=500) 

+
+ + + diff --git a/cov_html/z_d8907effad88fb1e_urls_py.html b/cov_html/z_d8907effad88fb1e_urls_py.html new file mode 100644 index 0000000000..17ddcba14f --- /dev/null +++ b/cov_html/z_d8907effad88fb1e_urls_py.html @@ -0,0 +1,112 @@ + + + + + Coverage for oc_lettings_site\urls.py: 100% + + + + + +
+
+

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

+ +

+ 4 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-23 08:21 +0200 +

+ +
+
+
+

1""" 

+

2URLs module for oc_lettings_site app. 

+

3Include the lettings and profiles apps urls 

+

4""" 

+

5from django.contrib import admin 

+

6from django.urls import path, include 

+

7 

+

8from . import views 

+

9 

+

10urlpatterns = [ 

+

11 path('', views.index, name='index'), 

+

12 path('lettings/', include('lettings.urls')), 

+

13 path('profiles/', include('profiles.urls')), 

+

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

+

15] 

+
+ + + diff --git a/cov_html/z_d8907effad88fb1e_views_py.html b/cov_html/z_d8907effad88fb1e_views_py.html new file mode 100644 index 0000000000..a13744698f --- /dev/null +++ b/cov_html/z_d8907effad88fb1e_views_py.html @@ -0,0 +1,136 @@ + + + + + Coverage for oc_lettings_site\views.py: 100% + + + + + +
+
+

+ Coverage for oc_lettings_site \ views.py: + 100% +

+ +

+ 12 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.14.0, + created at 2026-05-28 09:03 +0200 +

+ +
+
+
+

1""" 

+

2Views module for oc_lettings_site app 

+

3""" 

+

4from django.http import HttpRequest, HttpResponse 

+

5from django.shortcuts import render 

+

6 

+

7from monitoring import init_sentry, logger 

+

8 

+

9 

+

10# Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque molestie quam lobortis leo 

+

11# consectetur ullamcorper non id est. Praesent dictum, nulla eget feugiat sagittis, sem mi 

+

12# convallis eros, vitae dapibus nisi lorem dapibus sem. Maecenas pharetra purus ipsum, eget 

+

13# consequat ipsum lobortis quis. Phasellus eleifend ex auctor venenatis tempus. Aliquam vitae erat 

+

14# ac orci placerat luctus. Nullam elementum urna nisi, pellentesque iaculis enim cursus in. 

+

15# Praesent volutpat porttitor magna, non finibus neque cursus id. 

+

16def index(request: HttpRequest) -> HttpResponse: 

+

17 """ 

+

18 View function for home page 

+

19 Args: 

+

20 request (HttpRequest): Http Request object 

+

21 

+

22 Returns: 

+

23 An HTTP response with index page or HTTP response with 500 error. 

+

24 """ 

+

25 init_sentry() 

+

26 try: 

+

27 logger.info(f"Going to home page : status = 200.") 

+

28 

+

29 return render(request=request, template_name='oc_lettings_site/index.html') 

+

30 

+

31 except Exception as e: 

+

32 context = {'error': str(e)} 

+

33 

+

34 logger.error(f"Error 500 returned while reaching home page : {context=}" 

+

35 f", status = 500.") 

+

36 

+

37 return render(request=request, 

+

38 template_name='oc_lettings_site/error_500.html', 

+

39 context=context) 

+
+ + + diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000000..d0c3cbf102 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/build/doctrees/db_structure_and_models.doctree b/docs/build/doctrees/db_structure_and_models.doctree new file mode 100644 index 0000000000..37394934ab Binary files /dev/null and b/docs/build/doctrees/db_structure_and_models.doctree differ diff --git a/docs/build/doctrees/deployment.doctree b/docs/build/doctrees/deployment.doctree new file mode 100644 index 0000000000..80ccda4a61 Binary files /dev/null and b/docs/build/doctrees/deployment.doctree differ diff --git a/docs/build/doctrees/description.doctree b/docs/build/doctrees/description.doctree new file mode 100644 index 0000000000..26976a97f7 Binary files /dev/null and b/docs/build/doctrees/description.doctree differ diff --git a/docs/build/doctrees/documentation.doctree b/docs/build/doctrees/documentation.doctree new file mode 100644 index 0000000000..08b053788a Binary files /dev/null and b/docs/build/doctrees/documentation.doctree differ diff --git a/docs/build/doctrees/environment.pickle b/docs/build/doctrees/environment.pickle new file mode 100644 index 0000000000..88834ece6e Binary files /dev/null and b/docs/build/doctrees/environment.pickle differ diff --git a/docs/build/doctrees/index.doctree b/docs/build/doctrees/index.doctree new file mode 100644 index 0000000000..0541ed28f8 Binary files /dev/null and b/docs/build/doctrees/index.doctree differ diff --git a/docs/build/doctrees/installation.doctree b/docs/build/doctrees/installation.doctree new file mode 100644 index 0000000000..50fd60e42e Binary files /dev/null and b/docs/build/doctrees/installation.doctree differ diff --git a/docs/build/doctrees/links.doctree b/docs/build/doctrees/links.doctree new file mode 100644 index 0000000000..15632e4609 Binary files /dev/null and b/docs/build/doctrees/links.doctree differ diff --git a/docs/build/doctrees/programing_interface.doctree b/docs/build/doctrees/programing_interface.doctree new file mode 100644 index 0000000000..137fe83cf0 Binary files /dev/null and b/docs/build/doctrees/programing_interface.doctree differ diff --git a/docs/build/doctrees/quality.doctree b/docs/build/doctrees/quality.doctree new file mode 100644 index 0000000000..829e2936db Binary files /dev/null and b/docs/build/doctrees/quality.doctree differ diff --git a/docs/build/doctrees/quick_start.doctree b/docs/build/doctrees/quick_start.doctree new file mode 100644 index 0000000000..c31a4177c2 Binary files /dev/null and b/docs/build/doctrees/quick_start.doctree differ diff --git a/docs/build/doctrees/stack.doctree b/docs/build/doctrees/stack.doctree new file mode 100644 index 0000000000..3912b33f28 Binary files /dev/null and b/docs/build/doctrees/stack.doctree differ diff --git a/docs/build/doctrees/use_cases.doctree b/docs/build/doctrees/use_cases.doctree new file mode 100644 index 0000000000..82a75df38b Binary files /dev/null and b/docs/build/doctrees/use_cases.doctree differ diff --git a/docs/build/html/.buildinfo b/docs/build/html/.buildinfo new file mode 100644 index 0000000000..7124b340e1 --- /dev/null +++ b/docs/build/html/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 75ec20610af346382f4fadfa810782cc +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/build/html/_images/cov_report_1_screenshot.png b/docs/build/html/_images/cov_report_1_screenshot.png new file mode 100644 index 0000000000..8da9ced1d2 Binary files /dev/null and b/docs/build/html/_images/cov_report_1_screenshot.png differ diff --git a/docs/build/html/_images/database_tables_1_screenshot.png b/docs/build/html/_images/database_tables_1_screenshot.png new file mode 100644 index 0000000000..0efa8af694 Binary files /dev/null and b/docs/build/html/_images/database_tables_1_screenshot.png differ diff --git a/docs/build/html/_images/home_page_screenshot.png b/docs/build/html/_images/home_page_screenshot.png new file mode 100644 index 0000000000..798f0950ee Binary files /dev/null and b/docs/build/html/_images/home_page_screenshot.png differ diff --git a/docs/build/html/_images/letting_404_error_screenshot.png b/docs/build/html/_images/letting_404_error_screenshot.png new file mode 100644 index 0000000000..686e8aa997 Binary files /dev/null and b/docs/build/html/_images/letting_404_error_screenshot.png differ diff --git a/docs/build/html/_images/letting_details_screenshot.png b/docs/build/html/_images/letting_details_screenshot.png new file mode 100644 index 0000000000..ab0dbd83bb Binary files /dev/null and b/docs/build/html/_images/letting_details_screenshot.png differ diff --git a/docs/build/html/_images/lettings_list_screenshot.png b/docs/build/html/_images/lettings_list_screenshot.png new file mode 100644 index 0000000000..21f41d766f Binary files /dev/null and b/docs/build/html/_images/lettings_list_screenshot.png differ diff --git a/docs/build/html/_images/logo.png b/docs/build/html/_images/logo.png new file mode 100644 index 0000000000..47ecb49b49 Binary files /dev/null and b/docs/build/html/_images/logo.png differ diff --git a/docs/build/html/_images/profile_details_screenshot.png b/docs/build/html/_images/profile_details_screenshot.png new file mode 100644 index 0000000000..57ca31328d Binary files /dev/null and b/docs/build/html/_images/profile_details_screenshot.png differ diff --git a/docs/build/html/_images/profiles_list_screenshot.png b/docs/build/html/_images/profiles_list_screenshot.png new file mode 100644 index 0000000000..835b72dbdd Binary files /dev/null and b/docs/build/html/_images/profiles_list_screenshot.png differ diff --git a/docs/build/html/_images/structure_screenshot.png b/docs/build/html/_images/structure_screenshot.png new file mode 100644 index 0000000000..af2dd62183 Binary files /dev/null and b/docs/build/html/_images/structure_screenshot.png differ diff --git a/docs/build/html/_sources/db_structure_and_models.rst.txt b/docs/build/html/_sources/db_structure_and_models.rst.txt new file mode 100644 index 0000000000..a5533b545b --- /dev/null +++ b/docs/build/html/_sources/db_structure_and_models.rst.txt @@ -0,0 +1,67 @@ +Database structure and models +============================= + +The code has been split in 3 different apps vs only one in the old version : + +* lettings +* oc_lettings_site +* profiles + +Each new app has 5 main modules as below : + +* admin.py for the representation in admin page +* apps.py for the app namespace +* models.py for the models used by the database (except for oc_lettings_app) +* urls.py for the urls +* views.py for the views + +Lettings models +--------------- +In the lettings models there are 2 objects with all attributes regarding the address and letting, previously implemented in the first version of the application : + +* Address object + +================ ==================== +Attribute Type +================ ==================== +number PositiveIntegerField +street CharField +city CharField +state CharField +zip_code PositiveIntegerField +country_iso_code CharField +================ ==================== + +* Letting object + +========= ======================== +Attribute Type +========= ======================== +title CharField +address OneToOneField to Address +========= ======================== + +Oc_lettings_site models +----------------------- +No models are currently available in this application in its new version. + +This Django app serves only as an entry point of the web application and for Django settings. + +Profiles models +--------------- +In the profiles models, there is 1 object with all attributes regarding the profiles previously implemented in the first version of the application : + +* Profile object + +============= ===================== +Attribute Type +============= ===================== +user OneToOneField to User +favorite_city CharField +============= ===================== + +Database admin page +------------------- +Here is a screenshot from the database admin page : + +.. image:: _static/database_tables_1_screenshot.png diff --git a/docs/build/html/_sources/deployment.rst.txt b/docs/build/html/_sources/deployment.rst.txt new file mode 100644 index 0000000000..52b1d823f0 --- /dev/null +++ b/docs/build/html/_sources/deployment.rst.txt @@ -0,0 +1,266 @@ +Deployment +========== + +Django settings +--------------- +The Django application's `settings.py` file is used to configure the application regarding some very important +environment variables used by the CI/CD pipeline such as "DEBUG" and "DJANGO_ALLOWED_HOSTS" (the latter must be set on +your Heroku profile) and regarding the static files used by the application's front-end. + +Static files and WhiteNoise +--------------------------- + +You can find the detailed specific configuration of WhiteNoise library by checking the url below : `WhiteNoise `_ + +Configuration +^^^^^^^^^^^^^ +If you’re familiar with Django you’ll know what to do. If you’re just getting started with a new Django project then you’ll need add the following to the bottom of your settings.py file: + +.. code:: + + STATIC_ROOT = BASE_DIR / "staticfiles" + +Enable WhiteNoise +^^^^^^^^^^^^^^^^^ +The WhiteNoise library has to be installed before if not already done. +Please edit your `settings.py` file and add WhiteNoise to the MIDDLEWARE list. +The WhiteNoise middleware should be placed directly after the Django SecurityMiddleware (if you are using it) and before all other middleware: + +.. code:: + + MIDDLEWARE = [ + # ... + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + # ... + ] + +Add compression and caching support +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +WhiteNoise comes with a storage backend which compresses your files and hashes them to unique names, so they can safely be cached forever. To use it, set it as your staticfiles storage backend in your settings file: + +.. code:: + + STORAGES = { + # ... + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, + } + +Docker containerization +----------------------- +A Dockerfile in the repository root defines the process for building an image of the application and its dependencies +as a Docker container. Here are the main steps in this image building : + +* Copy the dependencies from the `requirements.txt` file and install them. + +* Retrieve the static files of the Django application. + +* Run Gunicorn (a library required for deployment on Heroku) on the specified application (for example, `oc_lettings_site.wsgi:application`), and the specified host and port. + +CI/CD pipeline +-------------- +GitHub Actions is the CI/CD solution provided by GitHub, while GitLab uses GitLab CI/CD pipelines. + +The `.yml` file structures the CI/CD pipelines and it is used by GitHub for creating GitHub Actions or used by Gitlab for creating CI/CD workflow. + +CI pipeline +^^^^^^^^^^^ +The continuous integration (CI) pipeline ensures the quality of the code. +The CI pipeline defined in the "ci" job runs as follows: + +* Checkout on the branch from which the push was performed + +* Installing the project's dependencies + +* Running linters (quality step) + +* Running tests with the `pytest` command (testing step) + +* Finally, loading the quality and test coverage reports + +CD pipeline +^^^^^^^^^^^ +The Continuous Delivery (CD) pipeline ensures the delivery and deployment of the app. +The CD pipeline defined in the "deploy" job works as follows: + +* Requires the successful execution of the previous continuous integration (CI) job (described earlier) on the master branch. + +* Checkout on the master branch. If the commit & push are performed from another branch, the CD pipeline is not executed. + +* The Docker image is initialized. + +* The Docker image is built and pushed (containerization process). + +* The Heroku command-line interface (CLI) is installed. + +* Deployment to Heroku. + +Secrets/variables +----------------- +In order to use correctly this Django web application, you must define some secrets in your Git platform secrets section. +Those secrets are used in the django.yml file that describes the CI/CD pipeline. + +GitHub +^^^^^^ + +Here's how you can add a secret on GitHub : + +* Go to your GitHub profile and open the project repository. + +* Click on "Settings", then on the "Secrets & Variables" section, and finally on the "Actions" button in the dropdown menu. + +* Next, click on "New Repository Secret" and enter the secret name and the secret value. Then confirm. The new secret is added to the repository. + +Gitlab +^^^^^^ + +Here's how you can add a secret variable on GitLab : + +* Go to your GitLab profile and open the project repository. + +* In the left sidebar, click on "Settings" and then on "CI/CD". + +* Expand the "Variables" section. + +* Click on "Add variable". + +* Enter the variable key (name) and the variable value, then click on "Add variable" to save it. + +* The new variable is now available in the GitLab CI/CD pipeline. + +Required secrets/variables +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here are all the required secrets/variables : + +* DOCKER_PASSWORD + +* DOCKER_USERNAME + +* HEROKU_API_KEY + +* HEROKU_USER_EMAIL + +* SENTRY_KEY + +When created, you can then use this secret with the variable `secrets` as shown below : + +.. code:: + + ${{ secrets. }} + +All secrets are used according to this format in the django.yml file which describes the CI/CD pipeline. + +The variable attribute name and the secret name has to be rigorously identical. + +Heroku +------ + +GitHub +^^^^^^ + +Once the GitHub actions are successfully completed, the application is deployed as a web service. Please check the +application's Heroku URL or click the "View application" button in your Heroku account to view the web application. + +If the GitHub actions failed, please review the logs on GitHub or in Heroku, fix the errors, and try the deployment +again. + +Gitlab +^^^^^^ + +Once the Gitlab CI/CD is successfully completed, the application is deployed as a web service. Please check the +application's Heroku URL or click the "View application" button in your Heroku account to view the web application. + +If the Gitlab CI/CD failed, please review the logs on the platform or in Heroku, fix the errors, and try the deployment +again. + +Environment variables +^^^^^^^^^^^^^^^^^^^^^ +The application needs some environment variables to work well. You must specify the variables into your Heroku app. +Please follow the steps below : + +* Go into your app in Heroku dashboard + +* Go to settings section + +* scroll down to "config vars" sub-section + +* click on the "Reveal Config Vars" button to reveal the variables if some are already existing + +* For each environment variable needed by the project, enter the "key" and the "value", and validate by clicking the "add' button + +Hera are all the variables regarding Django you need to provide : + +==================== ================================================================= +Key Value +==================== ================================================================= +DEBUG False (required for production deployment, True otherwise in dev) +DJANGO_ALLOWED_HOSTS localhost,127.0.0.1,.herokuapp.com +SECRET_KEY +==================== ================================================================= + +Monitoring with Sentry +---------------------- +In this project, the CI/CD pipeline uses the Sentry SDK for exceptions and logs monitoring. + +You will need a Sentry account to run the application correctly. Therefore, please register on the Sentry website +if you don't already have one. + +Another step: Before using the deployment pipeline, you must define a secret or variable into your Git platform for the Sentry key, required to link +the application to your Sentry account. Please see the "Secrets" subsection above. + +Troubleshooting +--------------- +In this section we focus on some common issues that arise in this kind of pipeline. + +Heroku application crashes immediately +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Here are the possible causes : + +* bad port + +* non-used port + +* bad Gunicorn configuration + + +Example : +If the application crashes immediately after deployment, ensure Gunicorn binds to the environment PORT variable: + +.. code:: bash + + gunicorn oc_lettings_site.wsgi:application --bind 0.0.0.0:$PORT + +ALLOWED_HOSTS errors +^^^^^^^^^^^^^^^^^^^^ +Example : +A HTTP 400 error usually indicates that the Heroku domain is missing from DJANGO_ALLOWED_HOSTS, environment variable +defines in Heroku app. + +.. code:: bash + + DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,my-app.herokuapp.com + +Static files not loading +^^^^^^^^^^^^^^^^^^^^^^^^ +Here are the possible causes : + +* collectstatic not implemented + +* WhiteNoise not used + +* STATIC_ROOT not defined in Django app settings.py file + +Docker build fails +^^^^^^^^^^^^^^^^^^ +Here are the possible causes : + +* secrets missing + +* invalid Heroku API key + +* bad Docker login + +* release failed diff --git a/docs/build/html/_sources/description.rst.txt b/docs/build/html/_sources/description.rst.txt new file mode 100644 index 0000000000..2cfcd961ff --- /dev/null +++ b/docs/build/html/_sources/description.rst.txt @@ -0,0 +1,50 @@ + +Description +=========== +Orange County Lettings is a startup in the real estate rental sector. The startup is currently expanding rapidly in the +United States. +The Orange County Lettings teams developed the +`OC_Letting_Site `_ web application and the new scaled +version has just been released. + +Summary +------- + +The new version has been scaled using a modular architecture. + +What we have done : + +* Redesign of the modular architecture in the Git platform repository +* Reduction of various technical debts on the project +* Addition and deployment of a CI/CD pipeline +* Application monitoring and error tracking via Sentry +* Creation of the application's technical documentation using Read The Docs and Sphinx + +The application must : + +* allow the users to view available rentals and all the registered profiles. + +Architecture +------------ + +Overview +^^^^^^^^ +.. image:: _static/structure_screenshot.png + +Modular architecture +^^^^^^^^^^^^^^^^^^^^ +The architecture has been optimized by reducing the technical debts from the previous monolithic design. + +The code has been : + +* reorganized into several separate Django applications +* reorganized into application-specific HTML templates folders + +This optimization has improved the flexibility, maintainability, and scalability of the code. + +Finally, each app has its own : + +* views module +* urls module +* templates folder +* test folder with several test modules for models, views and urls diff --git a/docs/build/html/_sources/documentation.rst.txt b/docs/build/html/_sources/documentation.rst.txt new file mode 100644 index 0000000000..3a5d4e4d9a --- /dev/null +++ b/docs/build/html/_sources/documentation.rst.txt @@ -0,0 +1,62 @@ +Documentation +============= + +Environment configuration +------------------------- +In the `env-docs` virtual environment, you must install Sphinx library if not already installed as explained in the :doc:`installation section `. + +.. code:: + + pip install sphinx + +.. code:: + + poetry add sphinx + +.. code:: + + uv add sphinx + +Documentation project initialization +------------------------------------ +Please create a "docs" folder or use one if existing and type in your terminal the command below to initialize the documentation : + +.. code:: + + sphinx-quickstart + +You can choose default options and validate. It will create all required files used by ReadTheDocs documentation. + +Documentation editing +--------------------- +You can use the index.rst as an entry point for the documentation content. Please use reStructuredText language in order to add content. +If you need help, you can check the url below to see `reStructuredText help documentation `_. + +Documentation local building +---------------------------- +To generate your documentation locally while editing content, you must type in your terminal the command below : + +.. code:: + + .\docs\make.bat html + +ReadTheDocs configuration +------------------------- +Here are the steps to follow if you want to generate another documentation in your account into the ReadTheDocs website : + +* First, you need to create an account on the ReadTheDocs website if you don't already have one. + +* Then create a new documentation project and link it to your Git platform : + * Click on "Import a repository" and then "Connect to GitHub/GitLab" + * Click on "+" button to finalise the importation + * The two accounts are synchronized + +* Configure the options of the ReadTheDocs projects + +* Configure the option for the automatic building and release after each push & commit on GitHub/Gitlab + +* Finally, ReadTheDocs will detect each modification and will rebuild the documentation online + +ReadTheDocs documentation access +-------------------------------- +ReadTheDocs provides an URL to your documentation online. You can include that URL in your repository `readme.md` file. diff --git a/docs/build/html/_sources/index.rst.txt b/docs/build/html/_sources/index.rst.txt new file mode 100644 index 0000000000..5d289d9824 --- /dev/null +++ b/docs/build/html/_sources/index.rst.txt @@ -0,0 +1,29 @@ +.. Orange County Lettings documentation master file, created by + sphinx-quickstart on Tue May 26 16:37:54 2026. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +=============================================== +Welcome to Orange County Lettings documentation +=============================================== + +.. image:: _static/logo.png + +------------------------------------------------------------------- +By `Nicolas MARIE `_ - June 2026 - v1.0 +------------------------------------------------------------------- + +.. toctree:: + :maxdepth: 2 + + description + stack + installation + quick_start + db_structure_and_models + quality + programing_interface + use_cases + deployment + documentation + links diff --git a/docs/build/html/_sources/installation.rst.txt b/docs/build/html/_sources/installation.rst.txt new file mode 100644 index 0000000000..b648d8143f --- /dev/null +++ b/docs/build/html/_sources/installation.rst.txt @@ -0,0 +1,119 @@ +Installation +============ + +Python version +-------------- + +For this new app version, the Python version is the same : Python 3.10 + +Virtual environments +-------------------- +In this Django application we use 2 virtual environments as below : + +* One for the application environment (env) + +* One for the ReadTheDocs documentation environment (env-docs) + +Libraries +--------- +The libraries used are for both environments : + +App env +^^^^^^^ +* django (==3.0) + +* flake8 (==3.7.0) + +* flake8-html (==0.4.3) + +* pytest (==9.0.3) + +* pytest-django (==4.12.0) + +* pytest-cov (==7.1.0) + +* six (==1.17.0) + +* sentry-sdk (>=2.60.0,<3.0.0) + +* python-dotenv (>=1.2.2,<2.0.0) + +* gunicorn (>=26.0.0,<27.0.0) + +* whitenoise (>=6.12.0,<7.0.0) + +Documentation env +^^^^^^^^^^^^^^^^^ +* Sphinx (==8.1.3) + +* sphinx_rtd_theme (==3.1.0) + +Dependency manager and installation +----------------------------------- + +Pip +^^^ + +First, create the virtual environment : +.. code:: + + py -3.10 -m venv env + +Then, activate the virtual env : + +* in Git Bash on Windows or on macOS / Linux + +.. code:: + + source env/bin/activate + + +* on Windows + +.. code:: + + env\Scripts\activate + +To install dependencies, type : + +.. code:: + + pip install -r requirements.txt + +Uv +^^ + +UV is an environment and dependencies manager. + +To install environment and dependencies, type : + +.. code:: + + uv sync + +UV will use the .toml file to know which Python version and dependencies to install. + +Poetry +^^^^^^ + +POETRY is an environment and dependencies manager. + +First, install the virtual environment : + +.. code:: + + py -3.10 -m venv env + +Then, activate the virtual env : + +.. code:: + + poetry env activate + +To install dependencies, type : + +.. code:: + + poetry install + +POETRY will use the .toml file to know which dependencies to install. diff --git a/docs/build/html/_sources/links.rst.txt b/docs/build/html/_sources/links.rst.txt new file mode 100644 index 0000000000..ab41e02e90 --- /dev/null +++ b/docs/build/html/_sources/links.rst.txt @@ -0,0 +1,23 @@ +Links +===== +You can find more details on each library used in the Orange County Lettings application by checking the links below : + +* `Django `_ + +* `WhiteNoise `_ + +* `Flake8 `_ + +* `Flake8-html `_ + +* `Pytest `_ + +* `Pytest-cov `_ + +* `Pytest-django `_ + +* `Sentry-sdk `_ + +* `Docker `_ + +* `Heroku with Docker `_ diff --git a/docs/build/html/_sources/programing_interface.rst.txt b/docs/build/html/_sources/programing_interface.rst.txt new file mode 100644 index 0000000000..f48e4b8bc8 --- /dev/null +++ b/docs/build/html/_sources/programing_interface.rst.txt @@ -0,0 +1,73 @@ +Programing Interface description +================================ + +URL routes +---------- +Here are all the routes for each Django app : + +`oc_lettings_site` app +^^^^^^^^^^^^^^^^^^^^^^ + +========== ============= +Route Description +========== ============= +/ Home page +/lettings/ Lettings list +/profiles/ Profiles list +/admin/ Admin page +========== ============= + +`lettings` app +^^^^^^^^^^^^^^ + +=========================== =================== +Route Description +=========================== =================== +/lettings/ Lettings index page +/lettings// Letting detail +=========================== =================== + +`profiles` app +^^^^^^^^^^^^^^ + +=========================== =================== +Route Description +=========================== =================== +/profiles/ Profiles index page +/profiles// Profile detail +=========================== =================== + +Django views +------------ +Each app has its views.py module according to the routes previously described. + +================ ======== ===================== +Django App View Template rendered +================ ======== ===================== +oc_lettings_site index index.html +lettings index lettings/index.html +lettings lettings lettings/letting.html +profiles index profiles/index.html +profiles profiles lettings/profile.html +================ ======== ===================== + +In addition, in each application view, we check if an error has occurred (404 or 500) and, if an error occurs, the view renders an error template. + +Templates rendering +------------------- +Each view renders templates with Django render method. + +Each Django app has a template folder with one template for each view previously described and one for each possible error. + +Database interaction +-------------------- +In all views, database access is achieved through an SQL query applied on the required model. The primary use case +involves calling the methods 'objects.all()' or `objects.get()` as shown below for example : + +.. code:: + + lettings_list = Letting.objects.all() + +.. code:: + + profile = Profile.objects.get(user__username=username) diff --git a/docs/build/html/_sources/quality.rst.txt b/docs/build/html/_sources/quality.rst.txt new file mode 100644 index 0000000000..eaf3fec098 --- /dev/null +++ b/docs/build/html/_sources/quality.rst.txt @@ -0,0 +1,90 @@ +Quality +======= + +Testing +------- +This Django project uses the Pytest library for testing the application. + +Each Django app has a testing package with commonly 3 testing modules (for models, urls, and views) and a test fixture module `conftest.py`. + +We guarantee 100% test coverage for the Orange County Lettings application in its new version. + +.. image:: _static/cov_report_1_screenshot.png + +Environment configuration +^^^^^^^^^^^^^^^^^^^^^^^^^ +In the `env` virtual environment, you must install Pytest library if not already installed as explained in the :doc:`installation section `. + +.. code:: + + pip install pytest + +.. code:: + + poetry add pytest + +.. code:: + + uv add pytest + +Tests coverage +^^^^^^^^^^^^^^ +To complete the test process, the `pytest-cov` library is used to generate a coverage report. + +To generate another report, you must install `pytest-cov` before. +Please use the same procedure as above for `pytest`. + +Tests local execution +^^^^^^^^^^^^^^^^^^^^^ +To generate another test process in your terminal, please type the line below : + +.. code:: + + pytest -v --cov=lettings --cov=profiles --cov=oc_lettings_site --cov-report=html:cov_html + +Automatic tests execution +^^^^^^^^^^^^^^^^^^^^^^^^^ +The Orange County Lettings application uses a CI/CD pipeline (detailed in :doc:`deployment section `) that automatically runs tests during the continuous integration task. + +This pipeline uses a `setup.cfg` file containing the test command that generates a new report after each commit and push to your Git platform. + +Linting +------- +This Django project uses the Flake 8 linter that ensures you to implement an always-standardized code and according to the PEP 8 convention. + +Environment configuration +^^^^^^^^^^^^^^^^^^^^^^^^^ +In the `env` virtual environment, you must install Flake 8 library if not already installed as explained in the :doc:`installation section `. + +.. code:: + + pip install flake8 + +.. code:: + + poetry add flake8 + +.. code:: + + uv add flake8 + +Flake 8 report +^^^^^^^^^^^^^^ +To complete the flake 8 process by adding an HTML report, the `flake8-html` library is used to. + +To generate another report, you must install `flake8-html` before. +Please use the same procedure as above for `flake8`. + +Flake 8 local execution +^^^^^^^^^^^^^^^^^^^^^^^ +To generate another flake 8 linting process in your terminal, please type the line below : + +.. code:: + + flake8 --format=html --htmldir=flake8-report --max-line-length=119 --extend-exclude="env/, env-docs/" + +Automatic linting +^^^^^^^^^^^^^^^^^ +The Orange County Lettings application uses a CI/CD pipeline (detailed in :doc:`deployment section `) that automatically runs Flake 8 linter during the continuous integration task. + +This pipeline uses a `setup.cfg` file containing the Flake 8 command to generate a new report after each commit and push to your Git platform. diff --git a/docs/build/html/_sources/quick_start.rst.txt b/docs/build/html/_sources/quick_start.rst.txt new file mode 100644 index 0000000000..4890cf8918 --- /dev/null +++ b/docs/build/html/_sources/quick_start.rst.txt @@ -0,0 +1,69 @@ +Quick start guide +================= + +Launching server +---------------- + +Local server +^^^^^^^^^^^^ +Please follow the steps as below : + +* Open a terminal +* Go to project folder - example : + + .. code:: + + cd oc_lettings_site + +* Activate the virtual environment as described previously +* Create environment variables (to avoid to add raw Sentry key into the code) : + + * With Power Shell : + + .. code:: + + $env:SENTRY_KEY = "your_key" + + * With Git Bash : + + .. code:: + + export SENTRY_KEY = "your_key" + +* Launch the local server by typing the command : + + .. code:: + + python manage.py runserver + +Web server +^^^^^^^^^^ + +Please follow the procedure described in :doc:`deployment section ` regarding the GitHub Actions or Gitlab CI/CD workflow, Docker containerization +and automatic deployment. + +Launching the APP +----------------- + +Please follow the steps as below : + +* With local server, open a web browser and type the urls : + + .. code:: + + http://127.0.0.1:8000/ + + .. code:: + + http://127.0.0.1:8000/admin + + for the admin panel (username: `admin`, password: given in the project technical specifications) + +* With web server (after deployment), open a web browser and type the url : + + * Your Heroku app url given in the Heroku dashboard, for example the url below : + + `Heroku app url example `_ + +You also need to specify all required environment variables used by the application in your Heroku app. +Please follow the procedure detailed in the Heroku sub-section in :doc:`deployment section `. diff --git a/docs/build/html/_sources/stack.rst.txt b/docs/build/html/_sources/stack.rst.txt new file mode 100644 index 0000000000..a6f92d5ec4 --- /dev/null +++ b/docs/build/html/_sources/stack.rst.txt @@ -0,0 +1,11 @@ +Stack +===== +The stack used for developing Orange County Lettings new version is as below : + +* Python 3.10 and some libraries (:doc:`see installation section `) for app code +* Pytest library for testing +* SQLite database +* Docker for image containerization +* Heroku (and Gunicorn library) for web deployment +* Sentry for logs and exceptions monitoring +* ReadTheDocs for the application documentation diff --git a/docs/build/html/_sources/use_cases.rst.txt b/docs/build/html/_sources/use_cases.rst.txt new file mode 100644 index 0000000000..4aca5b3224 --- /dev/null +++ b/docs/build/html/_sources/use_cases.rst.txt @@ -0,0 +1,68 @@ +User guide with use cases +========================= + +Quick overview +-------------- +Orange County Lettings allows the user to browse available rentals and all the registered profiles. + +The user has access to lettings and profiles lists, and can display a letting or a profile detail by id or username. + +Example of main use cases +------------------------- + +Home page +^^^^^^^^^ +.. image:: _static/home_page_screenshot.png + +Lettings list page +^^^^^^^^^^^^^^^^^^ +.. image:: _static/lettings_list_screenshot.png + +Letting details page +^^^^^^^^^^^^^^^^^^^^ +.. image:: _static/letting_details_screenshot.png + +Profiles list page +^^^^^^^^^^^^^^^^^^ +.. image:: _static/profiles_list_screenshot.png + +Profile details page +^^^^^^^^^^^^^^^^^^^^ +.. image:: _static/profile_details_screenshot.png + +Letting 404 error page +^^^^^^^^^^^^^^^^^^^^^^ +.. image:: _static/letting_404_error_screenshot.png + +Use cases +--------- + +Browsing lettings +^^^^^^^^^^^^^^^^^ +Here is the procedure : + +* Open the home page + +* Click on the "Letting" link + +* Select a letting from the list + +* The detail page displays the address and related information + +Viewing profiles +^^^^^^^^^^^^^^^^ +The procedure is more or less the same : + +* Open the Profiles page. + +* Select a profile from the list. + +* The application displays the user profile details. + +Returning to home page +^^^^^^^^^^^^^^^^^^^^^^ +Here is the procedure : + +* In the lettings page or profiles page, click on the "Home" button + +* The application displays the home page diff --git a/docs/build/html/_static/_sphinx_javascript_frameworks_compat.js b/docs/build/html/_static/_sphinx_javascript_frameworks_compat.js new file mode 100644 index 0000000000..81415803ec --- /dev/null +++ b/docs/build/html/_static/_sphinx_javascript_frameworks_compat.js @@ -0,0 +1,123 @@ +/* Compatability shim for jQuery and underscores.js. + * + * Copyright Sphinx contributors + * Released under the two clause BSD licence + */ + +/** + * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL + */ +jQuery.urldecode = function(x) { + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + var bbox = node.parentElement.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} diff --git a/docs/build/html/_static/basic.css b/docs/build/html/_static/basic.css new file mode 100644 index 0000000000..7ebbd6d07b --- /dev/null +++ b/docs/build/html/_static/basic.css @@ -0,0 +1,914 @@ +/* + * Sphinx stylesheet -- basic theme. + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin-top: 10px; +} + +ul.search li { + padding: 5px 0; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/build/html/_static/cov_report_1_screenshot.png b/docs/build/html/_static/cov_report_1_screenshot.png new file mode 100644 index 0000000000..8da9ced1d2 Binary files /dev/null and b/docs/build/html/_static/cov_report_1_screenshot.png differ diff --git a/docs/build/html/_static/css/badge_only.css b/docs/build/html/_static/css/badge_only.css new file mode 100644 index 0000000000..88ba55b965 --- /dev/null +++ b/docs/build/html/_static/css/badge_only.css @@ -0,0 +1 @@ +.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px} \ No newline at end of file diff --git a/docs/build/html/_static/css/fonts/Roboto-Slab-Bold.woff b/docs/build/html/_static/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 0000000000..6cb6000018 Binary files /dev/null and b/docs/build/html/_static/css/fonts/Roboto-Slab-Bold.woff differ diff --git a/docs/build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 b/docs/build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 0000000000..7059e23142 Binary files /dev/null and b/docs/build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/docs/build/html/_static/css/fonts/Roboto-Slab-Regular.woff b/docs/build/html/_static/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 0000000000..f815f63f99 Binary files /dev/null and b/docs/build/html/_static/css/fonts/Roboto-Slab-Regular.woff differ diff --git a/docs/build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 b/docs/build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 0000000000..f2c76e5bda Binary files /dev/null and b/docs/build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/docs/build/html/_static/css/fonts/fontawesome-webfont.eot b/docs/build/html/_static/css/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000000..e9f60ca953 Binary files /dev/null and b/docs/build/html/_static/css/fonts/fontawesome-webfont.eot differ diff --git a/docs/build/html/_static/css/fonts/fontawesome-webfont.svg b/docs/build/html/_static/css/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000000..855c845e53 --- /dev/null +++ b/docs/build/html/_static/css/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/build/html/_static/css/fonts/fontawesome-webfont.ttf b/docs/build/html/_static/css/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000000..35acda2fa1 Binary files /dev/null and b/docs/build/html/_static/css/fonts/fontawesome-webfont.ttf differ diff --git a/docs/build/html/_static/css/fonts/fontawesome-webfont.woff b/docs/build/html/_static/css/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000000..400014a4b0 Binary files /dev/null and b/docs/build/html/_static/css/fonts/fontawesome-webfont.woff differ diff --git a/docs/build/html/_static/css/fonts/fontawesome-webfont.woff2 b/docs/build/html/_static/css/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000000..4d13fc6040 Binary files /dev/null and b/docs/build/html/_static/css/fonts/fontawesome-webfont.woff2 differ diff --git a/docs/build/html/_static/css/fonts/lato-bold-italic.woff b/docs/build/html/_static/css/fonts/lato-bold-italic.woff new file mode 100644 index 0000000000..88ad05b9ff Binary files /dev/null and b/docs/build/html/_static/css/fonts/lato-bold-italic.woff differ diff --git a/docs/build/html/_static/css/fonts/lato-bold-italic.woff2 b/docs/build/html/_static/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 0000000000..c4e3d804b5 Binary files /dev/null and b/docs/build/html/_static/css/fonts/lato-bold-italic.woff2 differ diff --git a/docs/build/html/_static/css/fonts/lato-bold.woff b/docs/build/html/_static/css/fonts/lato-bold.woff new file mode 100644 index 0000000000..c6dff51f06 Binary files /dev/null and b/docs/build/html/_static/css/fonts/lato-bold.woff differ diff --git a/docs/build/html/_static/css/fonts/lato-bold.woff2 b/docs/build/html/_static/css/fonts/lato-bold.woff2 new file mode 100644 index 0000000000..bb195043cf Binary files /dev/null and b/docs/build/html/_static/css/fonts/lato-bold.woff2 differ diff --git a/docs/build/html/_static/css/fonts/lato-normal-italic.woff b/docs/build/html/_static/css/fonts/lato-normal-italic.woff new file mode 100644 index 0000000000..76114bc033 Binary files /dev/null and b/docs/build/html/_static/css/fonts/lato-normal-italic.woff differ diff --git a/docs/build/html/_static/css/fonts/lato-normal-italic.woff2 b/docs/build/html/_static/css/fonts/lato-normal-italic.woff2 new file mode 100644 index 0000000000..3404f37e2e Binary files /dev/null and b/docs/build/html/_static/css/fonts/lato-normal-italic.woff2 differ diff --git a/docs/build/html/_static/css/fonts/lato-normal.woff b/docs/build/html/_static/css/fonts/lato-normal.woff new file mode 100644 index 0000000000..ae1307ff5f Binary files /dev/null and b/docs/build/html/_static/css/fonts/lato-normal.woff differ diff --git a/docs/build/html/_static/css/fonts/lato-normal.woff2 b/docs/build/html/_static/css/fonts/lato-normal.woff2 new file mode 100644 index 0000000000..3bf9843328 Binary files /dev/null and b/docs/build/html/_static/css/fonts/lato-normal.woff2 differ diff --git a/docs/build/html/_static/css/theme.css b/docs/build/html/_static/css/theme.css new file mode 100644 index 0000000000..a88467c1b3 --- /dev/null +++ b/docs/build/html/_static/css/theme.css @@ -0,0 +1,4 @@ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search .wy-dropdown>aactive,.wy-side-nav-search .wy-dropdown>afocus,.wy-side-nav-search>a:hover,.wy-side-nav-search>aactive,.wy-side-nav-search>afocus{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon,.wy-side-nav-search>a.icon{display:block}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.switch-menus{position:relative;display:block;margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-side-nav-search>div.switch-menus>div.language-switch,.wy-side-nav-search>div.switch-menus>div.version-switch{display:inline-block;padding:.2em}.wy-side-nav-search>div.switch-menus>div.language-switch select,.wy-side-nav-search>div.switch-menus>div.version-switch select{display:inline-block;margin-right:-2rem;padding-right:2rem;max-width:240px;text-align-last:center;background:none;border:none;border-radius:0;box-shadow:none;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-size:1em;font-weight:400;color:hsla(0,0%,100%,.3);cursor:pointer;appearance:none;-webkit-appearance:none;-moz-appearance:none}.wy-side-nav-search>div.switch-menus>div.language-switch select:active,.wy-side-nav-search>div.switch-menus>div.language-switch select:focus,.wy-side-nav-search>div.switch-menus>div.language-switch select:hover,.wy-side-nav-search>div.switch-menus>div.version-switch select:active,.wy-side-nav-search>div.switch-menus>div.version-switch select:focus,.wy-side-nav-search>div.switch-menus>div.version-switch select:hover{background:hsla(0,0%,100%,.1);color:hsla(0,0%,100%,.5)}.wy-side-nav-search>div.switch-menus>div.language-switch select option,.wy-side-nav-search>div.switch-menus>div.version-switch select option{color:#000}.wy-side-nav-search>div.switch-menus>div.language-switch:has(>select):after,.wy-side-nav-search>div.switch-menus>div.version-switch:has(>select):after{display:inline-block;width:1.5em;height:100%;padding:.1em;content:"\f0d7";font-size:1em;line-height:1.2em;font-family:FontAwesome;text-align:center;pointer-events:none;box-sizing:border-box}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%;float:none;margin-left:0}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/docs/build/html/_static/database_tables_1_screenshot.png b/docs/build/html/_static/database_tables_1_screenshot.png new file mode 100644 index 0000000000..0efa8af694 Binary files /dev/null and b/docs/build/html/_static/database_tables_1_screenshot.png differ diff --git a/docs/build/html/_static/doctools.js b/docs/build/html/_static/doctools.js new file mode 100644 index 0000000000..0398ebb9f0 --- /dev/null +++ b/docs/build/html/_static/doctools.js @@ -0,0 +1,149 @@ +/* + * Base JavaScript utilities for all Sphinx HTML documentation. + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/docs/build/html/_static/documentation_options.js b/docs/build/html/_static/documentation_options.js new file mode 100644 index 0000000000..529239f071 --- /dev/null +++ b/docs/build/html/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '1.0', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/docs/build/html/_static/file.png b/docs/build/html/_static/file.png new file mode 100644 index 0000000000..a858a410e4 Binary files /dev/null and b/docs/build/html/_static/file.png differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bold.eot b/docs/build/html/_static/fonts/Lato/lato-bold.eot new file mode 100644 index 0000000000..3361183a41 Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-bold.eot differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bold.ttf b/docs/build/html/_static/fonts/Lato/lato-bold.ttf new file mode 100644 index 0000000000..29f691d5ed Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-bold.ttf differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bold.woff b/docs/build/html/_static/fonts/Lato/lato-bold.woff new file mode 100644 index 0000000000..c6dff51f06 Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-bold.woff differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bold.woff2 b/docs/build/html/_static/fonts/Lato/lato-bold.woff2 new file mode 100644 index 0000000000..bb195043cf Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-bold.woff2 differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bolditalic.eot b/docs/build/html/_static/fonts/Lato/lato-bolditalic.eot new file mode 100644 index 0000000000..3d4154936b Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-bolditalic.eot differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bolditalic.ttf b/docs/build/html/_static/fonts/Lato/lato-bolditalic.ttf new file mode 100644 index 0000000000..f402040b3e Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-bolditalic.ttf differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bolditalic.woff b/docs/build/html/_static/fonts/Lato/lato-bolditalic.woff new file mode 100644 index 0000000000..88ad05b9ff Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-bolditalic.woff differ diff --git a/docs/build/html/_static/fonts/Lato/lato-bolditalic.woff2 b/docs/build/html/_static/fonts/Lato/lato-bolditalic.woff2 new file mode 100644 index 0000000000..c4e3d804b5 Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-bolditalic.woff2 differ diff --git a/docs/build/html/_static/fonts/Lato/lato-italic.eot b/docs/build/html/_static/fonts/Lato/lato-italic.eot new file mode 100644 index 0000000000..3f826421a1 Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-italic.eot differ diff --git a/docs/build/html/_static/fonts/Lato/lato-italic.ttf b/docs/build/html/_static/fonts/Lato/lato-italic.ttf new file mode 100644 index 0000000000..b4bfc9b24a Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-italic.ttf differ diff --git a/docs/build/html/_static/fonts/Lato/lato-italic.woff b/docs/build/html/_static/fonts/Lato/lato-italic.woff new file mode 100644 index 0000000000..76114bc033 Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-italic.woff differ diff --git a/docs/build/html/_static/fonts/Lato/lato-italic.woff2 b/docs/build/html/_static/fonts/Lato/lato-italic.woff2 new file mode 100644 index 0000000000..3404f37e2e Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-italic.woff2 differ diff --git a/docs/build/html/_static/fonts/Lato/lato-regular.eot b/docs/build/html/_static/fonts/Lato/lato-regular.eot new file mode 100644 index 0000000000..11e3f2a5f0 Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-regular.eot differ diff --git a/docs/build/html/_static/fonts/Lato/lato-regular.ttf b/docs/build/html/_static/fonts/Lato/lato-regular.ttf new file mode 100644 index 0000000000..74decd9ebb Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-regular.ttf differ diff --git a/docs/build/html/_static/fonts/Lato/lato-regular.woff b/docs/build/html/_static/fonts/Lato/lato-regular.woff new file mode 100644 index 0000000000..ae1307ff5f Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-regular.woff differ diff --git a/docs/build/html/_static/fonts/Lato/lato-regular.woff2 b/docs/build/html/_static/fonts/Lato/lato-regular.woff2 new file mode 100644 index 0000000000..3bf9843328 Binary files /dev/null and b/docs/build/html/_static/fonts/Lato/lato-regular.woff2 differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot new file mode 100644 index 0000000000..79dc8efed3 Binary files /dev/null and b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf new file mode 100644 index 0000000000..df5d1df273 Binary files /dev/null and b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff new file mode 100644 index 0000000000..6cb6000018 Binary files /dev/null and b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 new file mode 100644 index 0000000000..7059e23142 Binary files /dev/null and b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot new file mode 100644 index 0000000000..2f7ca78a1e Binary files /dev/null and b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf new file mode 100644 index 0000000000..eb52a79073 Binary files /dev/null and b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff new file mode 100644 index 0000000000..f815f63f99 Binary files /dev/null and b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff differ diff --git a/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 new file mode 100644 index 0000000000..f2c76e5bda Binary files /dev/null and b/docs/build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 differ diff --git a/docs/build/html/_static/home_page_screenshot.png b/docs/build/html/_static/home_page_screenshot.png new file mode 100644 index 0000000000..798f0950ee Binary files /dev/null and b/docs/build/html/_static/home_page_screenshot.png differ diff --git a/docs/build/html/_static/jquery.js b/docs/build/html/_static/jquery.js new file mode 100644 index 0000000000..c4c6022f29 --- /dev/null +++ b/docs/build/html/_static/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t a.language.name.localeCompare(b.language.name)); + + const languagesHTML = ` +
+
Languages
+ ${languages + .map( + (translation) => ` +
+ ${translation.language.code} +
+ `, + ) + .join("\n")} +
+ `; + return languagesHTML; + } + + function renderVersions(config) { + if (!config.versions.active.length) { + return ""; + } + const versionsHTML = ` +
+
Versions
+ ${config.versions.active + .map( + (version) => ` +
+ ${version.slug} +
+ `, + ) + .join("\n")} +
+ `; + return versionsHTML; + } + + function renderDownloads(config) { + if (!Object.keys(config.versions.current.downloads).length) { + return ""; + } + const downloadsNameDisplay = { + pdf: "PDF", + epub: "Epub", + htmlzip: "HTML", + }; + + const downloadsHTML = ` +
+
Downloads
+ ${Object.entries(config.versions.current.downloads) + .map( + ([name, url]) => ` +
+ ${downloadsNameDisplay[name]} +
+ `, + ) + .join("\n")} +
+ `; + return downloadsHTML; + } + + document.addEventListener("readthedocs-addons-data-ready", function (event) { + const config = event.detail.data(); + + const flyout = ` +
+ + Read the Docs + v: ${config.versions.current.slug} + + +
+
+ ${renderLanguages(config)} + ${renderVersions(config)} + ${renderDownloads(config)} +
+
On Read the Docs
+
+ Project Home +
+
+ Builds +
+
+ Downloads +
+
+
+
Search
+
+
+ +
+
+
+
+ + Hosted by Read the Docs + +
+
+ `; + + // Inject the generated flyout into the body HTML element. + document.body.insertAdjacentHTML("beforeend", flyout); + + // Trigger the Read the Docs Addons Search modal when clicking on the "Search docs" input from inside the flyout. + document + .querySelector("#flyout-search-form") + .addEventListener("focusin", () => { + const event = new CustomEvent("readthedocs-search-show"); + document.dispatchEvent(event); + }); + }) +} + +if (themeLanguageSelector || themeVersionSelector) { + function onSelectorSwitch(event) { + const option = event.target.selectedIndex; + const item = event.target.options[option]; + window.location.href = item.dataset.url; + } + + document.addEventListener("readthedocs-addons-data-ready", function (event) { + const config = event.detail.data(); + + const versionSwitch = document.querySelector( + "div.switch-menus > div.version-switch", + ); + if (themeVersionSelector) { + let versions = config.versions.active; + if (config.versions.current.hidden || config.versions.current.type === "external") { + versions.unshift(config.versions.current); + } + const versionSelect = ` + + `; + + versionSwitch.innerHTML = versionSelect; + versionSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); + } + + const languageSwitch = document.querySelector( + "div.switch-menus > div.language-switch", + ); + + if (themeLanguageSelector) { + if (config.projects.translations.length) { + // Add the current language to the options on the selector + let languages = config.projects.translations.concat( + config.projects.current, + ); + languages = languages.sort((a, b) => + a.language.name.localeCompare(b.language.name), + ); + + const languageSelect = ` + + `; + + languageSwitch.innerHTML = languageSelect; + languageSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); + } + else { + languageSwitch.remove(); + } + } + }); +} + +document.addEventListener("readthedocs-addons-data-ready", function (event) { + // Trigger the Read the Docs Addons Search modal when clicking on "Search docs" input from the topnav. + document + .querySelector("[role='search'] input") + .addEventListener("focusin", () => { + const event = new CustomEvent("readthedocs-search-show"); + document.dispatchEvent(event); + }); +}); \ No newline at end of file diff --git a/docs/build/html/_static/language_data.js b/docs/build/html/_static/language_data.js new file mode 100644 index 0000000000..c7fe6c6faf --- /dev/null +++ b/docs/build/html/_static/language_data.js @@ -0,0 +1,192 @@ +/* + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/docs/build/html/_static/letting_404_error_screenshot.png b/docs/build/html/_static/letting_404_error_screenshot.png new file mode 100644 index 0000000000..686e8aa997 Binary files /dev/null and b/docs/build/html/_static/letting_404_error_screenshot.png differ diff --git a/docs/build/html/_static/letting_details_screenshot.png b/docs/build/html/_static/letting_details_screenshot.png new file mode 100644 index 0000000000..ab0dbd83bb Binary files /dev/null and b/docs/build/html/_static/letting_details_screenshot.png differ diff --git a/docs/build/html/_static/lettings_list_screenshot.png b/docs/build/html/_static/lettings_list_screenshot.png new file mode 100644 index 0000000000..21f41d766f Binary files /dev/null and b/docs/build/html/_static/lettings_list_screenshot.png differ diff --git a/docs/build/html/_static/logo.png b/docs/build/html/_static/logo.png new file mode 100644 index 0000000000..47ecb49b49 Binary files /dev/null and b/docs/build/html/_static/logo.png differ diff --git a/docs/build/html/_static/minus.png b/docs/build/html/_static/minus.png new file mode 100644 index 0000000000..d96755fdaf Binary files /dev/null and b/docs/build/html/_static/minus.png differ diff --git a/docs/build/html/_static/plus.png b/docs/build/html/_static/plus.png new file mode 100644 index 0000000000..7107cec93a Binary files /dev/null and b/docs/build/html/_static/plus.png differ diff --git a/docs/build/html/_static/profile_details_screenshot.png b/docs/build/html/_static/profile_details_screenshot.png new file mode 100644 index 0000000000..57ca31328d Binary files /dev/null and b/docs/build/html/_static/profile_details_screenshot.png differ diff --git a/docs/build/html/_static/profiles_list_screenshot.png b/docs/build/html/_static/profiles_list_screenshot.png new file mode 100644 index 0000000000..835b72dbdd Binary files /dev/null and b/docs/build/html/_static/profiles_list_screenshot.png differ diff --git a/docs/build/html/_static/pygments.css b/docs/build/html/_static/pygments.css new file mode 100644 index 0000000000..6f8b210a1c --- /dev/null +++ b/docs/build/html/_static/pygments.css @@ -0,0 +1,75 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #F00 } /* Error */ +.highlight .k { color: #008000; font-weight: bold } /* Keyword */ +.highlight .o { color: #666 } /* Operator */ +.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #9C6500 } /* Comment.Preproc */ +.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.highlight .gr { color: #E40000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #008400 } /* Generic.Inserted */ +.highlight .go { color: #717171 } /* Generic.Output */ +.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #04D } /* Generic.Traceback */ +.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #008000 } /* Keyword.Pseudo */ +.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #B00040 } /* Keyword.Type */ +.highlight .m { color: #666 } /* Literal.Number */ +.highlight .s { color: #BA2121 } /* Literal.String */ +.highlight .na { color: #687822 } /* Name.Attribute */ +.highlight .nb { color: #008000 } /* Name.Builtin */ +.highlight .nc { color: #00F; font-weight: bold } /* Name.Class */ +.highlight .no { color: #800 } /* Name.Constant */ +.highlight .nd { color: #A2F } /* Name.Decorator */ +.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #00F } /* Name.Function */ +.highlight .nl { color: #767600 } /* Name.Label */ +.highlight .nn { color: #00F; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #19177C } /* Name.Variable */ +.highlight .ow { color: #A2F; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #BBB } /* Text.Whitespace */ +.highlight .mb { color: #666 } /* Literal.Number.Bin */ +.highlight .mf { color: #666 } /* Literal.Number.Float */ +.highlight .mh { color: #666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666 } /* Literal.Number.Oct */ +.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ +.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ +.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ +.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #00F } /* Name.Function.Magic */ +.highlight .vc { color: #19177C } /* Name.Variable.Class */ +.highlight .vg { color: #19177C } /* Name.Variable.Global */ +.highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.highlight .vm { color: #19177C } /* Name.Variable.Magic */ +.highlight .il { color: #666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/build/html/_static/searchtools.js b/docs/build/html/_static/searchtools.js new file mode 100644 index 0000000000..2c774d17af --- /dev/null +++ b/docs/build/html/_static/searchtools.js @@ -0,0 +1,632 @@ +/* + * Sphinx JavaScript utilities for the full-text search. + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename, kind] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +// Global search result kind enum, used by themes to style search results. +class SearchResultKind { + static get index() { return "index"; } + static get object() { return "object"; } + static get text() { return "text"; } + static get title() { return "title"; } +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename, kind] = item; + + let listItem = document.createElement("li"); + // Add a class representing the item's type: + // can be used by a theme's CSS selector for styling + // See SearchResultKind for the class names. + listItem.classList.add(`kind-${kind}`); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = Documentation.ngettext( + "Search finished, found one page matching the search query.", + "Search finished, found ${resultCount} pages matching the search query.", + resultCount, + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.setAttribute("role", "list"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename, kind]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + SearchResultKind.title, + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + SearchResultKind.index, + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + SearchResultKind.object, + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + SearchResultKind.text, + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/docs/build/html/_static/sphinx_highlight.js b/docs/build/html/_static/sphinx_highlight.js new file mode 100644 index 0000000000..8a96c69a19 --- /dev/null +++ b/docs/build/html/_static/sphinx_highlight.js @@ -0,0 +1,154 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/docs/build/html/_static/structure_screenshot.png b/docs/build/html/_static/structure_screenshot.png new file mode 100644 index 0000000000..af2dd62183 Binary files /dev/null and b/docs/build/html/_static/structure_screenshot.png differ diff --git a/docs/build/html/db_structure_and_models.html b/docs/build/html/db_structure_and_models.html new file mode 100644 index 0000000000..16c800c94c --- /dev/null +++ b/docs/build/html/db_structure_and_models.html @@ -0,0 +1,221 @@ + + + + + + + + + Database structure and models — Orange County Lettings 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Database structure and models

+

The code has been split in 3 different apps vs only one in the old version :

+
    +
  • lettings

  • +
  • oc_lettings_site

  • +
  • profiles

  • +
+

Each new app has 5 main modules as below :

+
    +
  • admin.py for the representation in admin page

  • +
  • apps.py for the app namespace

  • +
  • models.py for the models used by the database (except for oc_lettings_app)

  • +
  • urls.py for the urls

  • +
  • views.py for the views

  • +
+
+

Lettings models

+

In the lettings models there are 2 objects with all attributes regarding the address and letting, previously implemented in the first version of the application :

+
    +
  • Address object

  • +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

Attribute

Type

number

PositiveIntegerField

street

CharField

city

CharField

state

CharField

zip_code

PositiveIntegerField

country_iso_code

CharField

+
    +
  • Letting object

  • +
+ + + + + + + + + + + + + + +

Attribute

Type

title

CharField

address

OneToOneField to Address

+
+
+

Oc_lettings_site models

+

No models are currently available in this application in its new version.

+

This Django app serves only as an entry point of the web application and for Django settings.

+
+
+

Profiles models

+

In the profiles models, there is 1 object with all attributes regarding the profiles previously implemented in the first version of the application :

+
    +
  • Profile object

  • +
+ + + + + + + + + + + + + + +

Attribute

Type

user

OneToOneField to User

favorite_city

CharField

+
+
+

Database admin page

+

Here is a screenshot from the database admin page :

+_images/database_tables_1_screenshot.png +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/build/html/deployment.html b/docs/build/html/deployment.html new file mode 100644 index 0000000000..e5ba54671f --- /dev/null +++ b/docs/build/html/deployment.html @@ -0,0 +1,381 @@ + + + + + + + + + Deployment — Orange County Lettings 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Deployment

+
+

Django settings

+

The Django application’s settings.py file is used to configure the application regarding some very important +environment variables used by the CI/CD pipeline such as “DEBUG” and “DJANGO_ALLOWED_HOSTS” (the latter must be set on +your Heroku profile) and regarding the static files used by the application’s front-end.

+
+
+

Static files and WhiteNoise

+

You can find the detailed specific configuration of WhiteNoise library by checking the url below : WhiteNoise

+
+

Configuration

+

If you’re familiar with Django you’ll know what to do. If you’re just getting started with a new Django project then you’ll need add the following to the bottom of your settings.py file:

+
STATIC_ROOT = BASE_DIR / "staticfiles"
+
+
+
+
+

Enable WhiteNoise

+

The WhiteNoise library has to be installed before if not already done. +Please edit your settings.py file and add WhiteNoise to the MIDDLEWARE list. +The WhiteNoise middleware should be placed directly after the Django SecurityMiddleware (if you are using it) and before all other middleware:

+
MIDDLEWARE = [
+    # ...
+    "django.middleware.security.SecurityMiddleware",
+    "whitenoise.middleware.WhiteNoiseMiddleware",
+    # ...
+]
+
+
+
+
+

Add compression and caching support

+

WhiteNoise comes with a storage backend which compresses your files and hashes them to unique names, so they can safely be cached forever. To use it, set it as your staticfiles storage backend in your settings file:

+
STORAGES = {
+    # ...
+    "staticfiles": {
+        "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
+    },
+}
+
+
+
+
+
+

Docker containerization

+

A Dockerfile in the repository root defines the process for building an image of the application and its dependencies +as a Docker container. Here are the main steps in this image building :

+
    +
  • Copy the dependencies from the requirements.txt file and install them.

  • +
  • Retrieve the static files of the Django application.

  • +
  • Run Gunicorn (a library required for deployment on Heroku) on the specified application (for example, oc_lettings_site.wsgi:application), and the specified host and port.

  • +
+
+
+

CI/CD pipeline

+

GitHub Actions is the CI/CD solution provided by GitHub, while GitLab uses GitLab CI/CD pipelines.

+

The .yml file structures the CI/CD pipelines and it is used by GitHub for creating GitHub Actions or used by Gitlab for creating CI/CD workflow.

+
+

CI pipeline

+

The continuous integration (CI) pipeline ensures the quality of the code. +The CI pipeline defined in the “ci” job runs as follows:

+
    +
  • Checkout on the branch from which the push was performed

  • +
  • Installing the project’s dependencies

  • +
  • Running linters (quality step)

  • +
  • Running tests with the pytest command (testing step)

  • +
  • Finally, loading the quality and test coverage reports

  • +
+
+
+

CD pipeline

+

The Continuous Delivery (CD) pipeline ensures the delivery and deployment of the app. +The CD pipeline defined in the “deploy” job works as follows:

+
    +
  • Requires the successful execution of the previous continuous integration (CI) job (described earlier) on the master branch.

  • +
  • Checkout on the master branch. If the commit & push are performed from another branch, the CD pipeline is not executed.

  • +
  • The Docker image is initialized.

  • +
  • The Docker image is built and pushed (containerization process).

  • +
  • The Heroku command-line interface (CLI) is installed.

  • +
  • Deployment to Heroku.

  • +
+
+
+
+

Secrets/variables

+

In order to use correctly this Django web application, you must define some secrets in your Git platform secrets section. +Those secrets are used in the django.yml file that describes the CI/CD pipeline.

+
+

GitHub

+

Here’s how you can add a secret on GitHub :

+
    +
  • Go to your GitHub profile and open the project repository.

  • +
  • Click on “Settings”, then on the “Secrets & Variables” section, and finally on the “Actions” button in the dropdown menu.

  • +
  • Next, click on “New Repository Secret” and enter the secret name and the secret value. Then confirm. The new secret is added to the repository.

  • +
+
+
+

Gitlab

+

Here’s how you can add a secret variable on GitLab :

+
    +
  • Go to your GitLab profile and open the project repository.

  • +
  • In the left sidebar, click on “Settings” and then on “CI/CD”.

  • +
  • Expand the “Variables” section.

  • +
  • Click on “Add variable”.

  • +
  • Enter the variable key (name) and the variable value, then click on “Add variable” to save it.

  • +
  • The new variable is now available in the GitLab CI/CD pipeline.

  • +
+
+
+

Required secrets/variables

+

Here are all the required secrets/variables :

+
    +
  • DOCKER_PASSWORD

  • +
  • DOCKER_USERNAME

  • +
  • HEROKU_API_KEY

  • +
  • HEROKU_USER_EMAIL

  • +
  • SENTRY_KEY

  • +
+

When created, you can then use this secret with the variable secrets as shown below :

+
${{ secrets.<THE SECRET NAME> }}
+
+
+

All secrets are used according to this format in the django.yml file which describes the CI/CD pipeline.

+

The variable attribute name and the secret name has to be rigorously identical.

+
+
+
+

Heroku

+
+

GitHub

+

Once the GitHub actions are successfully completed, the application is deployed as a web service. Please check the +application’s Heroku URL or click the “View application” button in your Heroku account to view the web application.

+

If the GitHub actions failed, please review the logs on GitHub or in Heroku, fix the errors, and try the deployment +again.

+
+
+

Gitlab

+

Once the Gitlab CI/CD is successfully completed, the application is deployed as a web service. Please check the +application’s Heroku URL or click the “View application” button in your Heroku account to view the web application.

+

If the Gitlab CI/CD failed, please review the logs on the platform or in Heroku, fix the errors, and try the deployment +again.

+
+
+

Environment variables

+

The application needs some environment variables to work well. You must specify the variables into your Heroku app. +Please follow the steps below :

+
    +
  • Go into your app in Heroku dashboard

  • +
  • Go to settings section

  • +
  • scroll down to “config vars” sub-section

  • +
  • click on the “Reveal Config Vars” button to reveal the variables if some are already existing

  • +
  • For each environment variable needed by the project, enter the “key” and the “value”, and validate by clicking the “add’ button

  • +
+

Hera are all the variables regarding Django you need to provide :

+ + + + + + + + + + + + + + + + + +

Key

Value

DEBUG

False (required for production deployment, True otherwise in dev)

DJANGO_ALLOWED_HOSTS

localhost,127.0.0.1,<your heroku url>.herokuapp.com

SECRET_KEY

<your Django secret key>

+
+
+
+

Monitoring with Sentry

+

In this project, the CI/CD pipeline uses the Sentry SDK for exceptions and logs monitoring.

+

You will need a Sentry account to run the application correctly. Therefore, please register on the Sentry website +if you don’t already have one.

+

Another step: Before using the deployment pipeline, you must define a secret or variable into your Git platform for the Sentry key, required to link +the application to your Sentry account. Please see the “Secrets” subsection above.

+
+
+

Troubleshooting

+

In this section we focus on some common issues that arise in this kind of pipeline.

+
+

Heroku application crashes immediately

+

Here are the possible causes :

+
    +
  • bad port

  • +
  • non-used port

  • +
  • bad Gunicorn configuration

  • +
+

Example : +If the application crashes immediately after deployment, ensure Gunicorn binds to the environment PORT variable:

+
gunicorn oc_lettings_site.wsgi:application --bind 0.0.0.0:$PORT
+
+
+
+
+

ALLOWED_HOSTS errors

+

Example : +A HTTP 400 error usually indicates that the Heroku domain is missing from DJANGO_ALLOWED_HOSTS, environment variable +defines in Heroku app.

+
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,my-app.herokuapp.com
+
+
+
+
+

Static files not loading

+

Here are the possible causes :

+
    +
  • collectstatic not implemented

  • +
  • WhiteNoise not used

  • +
  • STATIC_ROOT not defined in Django app settings.py file

  • +
+
+
+

Docker build fails

+

Here are the possible causes :

+
    +
  • secrets missing

  • +
  • invalid Heroku API key

  • +
  • bad Docker login

  • +
  • release failed

  • +
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/build/html/description.html b/docs/build/html/description.html new file mode 100644 index 0000000000..cb6ca79c3b --- /dev/null +++ b/docs/build/html/description.html @@ -0,0 +1,170 @@ + + + + + + + + + Description — Orange County Lettings 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Description

+

Orange County Lettings is a startup in the real estate rental sector. The startup is currently expanding rapidly in the +United States. +The Orange County Lettings teams developed the +OC_Letting_Site web application and the new scaled +version has just been released.

+
+

Summary

+

The new version has been scaled using a modular architecture.

+

What we have done :

+
    +
  • Redesign of the modular architecture in the Git platform repository

  • +
  • Reduction of various technical debts on the project

  • +
  • Addition and deployment of a CI/CD pipeline

  • +
  • Application monitoring and error tracking via Sentry

  • +
  • Creation of the application’s technical documentation using Read The Docs and Sphinx

  • +
+

The application must :

+
    +
  • allow the users to view available rentals and all the registered profiles.

  • +
+
+
+

Architecture

+
+

Overview

+_images/structure_screenshot.png +
+
+

Modular architecture

+

The architecture has been optimized by reducing the technical debts from the previous monolithic design.

+

The code has been :

+
    +
  • reorganized into several separate Django applications

  • +
  • reorganized into application-specific HTML templates folders

  • +
+

This optimization has improved the flexibility, maintainability, and scalability of the code.

+

Finally, each app has its own :

+
    +
  • views module

  • +
  • urls module

  • +
  • templates folder

  • +
  • test folder with several test modules for models, views and urls

  • +
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/build/html/documentation.html b/docs/build/html/documentation.html new file mode 100644 index 0000000000..12281b4893 --- /dev/null +++ b/docs/build/html/documentation.html @@ -0,0 +1,179 @@ + + + + + + + + + Documentation — Orange County Lettings 1.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Documentation

+
+

Environment configuration

+

In the env-docs virtual environment, you must install Sphinx library if not already installed as explained in the installation section.

+
pip install sphinx
+
+
+
poetry add sphinx
+
+
+
uv add sphinx
+
+
+
+
+

Documentation project initialization

+

Please create a “docs” folder or use one if existing and type in your terminal the command below to initialize the documentation :

+
sphinx-quickstart
+
+
+

You can choose default options and validate. It will create all required files used by ReadTheDocs documentation.

+
+
+

Documentation editing

+

You can use the index.rst as an entry point for the documentation content. Please use reStructuredText language in order to add content. +If you need help, you can check the url below to see reStructuredText help documentation.

+
+
+

Documentation local building

+

To generate your documentation locally while editing content, you must type in your terminal the command below :

+
.\docs\make.bat html
+
+
+
+
+

ReadTheDocs configuration

+

Here are the steps to follow if you want to generate another documentation in your account into the ReadTheDocs website :

+
    +
  • First, you need to create an account on the ReadTheDocs website if you don’t already have one.

  • +
  • +
    Then create a new documentation project and link it to your Git platform :
      +
    • Click on “Import a repository” and then “Connect to GitHub/GitLab”

    • +
    • Click on “+” button to finalise the importation

    • +
    • The two accounts are synchronized

    • +
    +
    +
    +
  • +
  • Configure the options of the ReadTheDocs projects

  • +
  • Configure the option for the automatic building and release after each push & commit on GitHub/Gitlab

  • +
  • Finally, ReadTheDocs will detect each modification and will rebuild the documentation online

  • +
+
+
+

ReadTheDocs documentation access

+

ReadTheDocs provides an URL to your documentation online. You can include that URL in your repository readme.md file.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/build/html/genindex.html b/docs/build/html/genindex.html new file mode 100644 index 0000000000..f34b90d74e --- /dev/null +++ b/docs/build/html/genindex.html @@ -0,0 +1,114 @@ + + + + + + + + Index — Orange County Lettings 1.0 documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Index

+ +
+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/build/html/index.html b/docs/build/html/index.html new file mode 100644 index 0000000000..06be556422 --- /dev/null +++ b/docs/build/html/index.html @@ -0,0 +1,188 @@ + + + + + + + + + Welcome to Orange County Lettings documentation — Orange County Lettings 1.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/docs/build/html/installation.html b/docs/build/html/installation.html new file mode 100644 index 0000000000..1c82f227ec --- /dev/null +++ b/docs/build/html/installation.html @@ -0,0 +1,225 @@ + + + + + + + + + Installation — Orange County Lettings 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Installation

+
+

Python version

+

For this new app version, the Python version is the same : Python 3.10

+
+
+

Virtual environments

+

In this Django application we use 2 virtual environments as below :

+
    +
  • One for the application environment (env)

  • +
  • One for the ReadTheDocs documentation environment (env-docs)

  • +
+
+
+

Libraries

+

The libraries used are for both environments :

+
+

App env

+
    +
  • django (==3.0)

  • +
  • flake8 (==3.7.0)

  • +
  • flake8-html (==0.4.3)

  • +
  • pytest (==9.0.3)

  • +
  • pytest-django (==4.12.0)

  • +
  • pytest-cov (==7.1.0)

  • +
  • six (==1.17.0)

  • +
  • sentry-sdk (>=2.60.0,<3.0.0)

  • +
  • python-dotenv (>=1.2.2,<2.0.0)

  • +
  • gunicorn (>=26.0.0,<27.0.0)

  • +
  • whitenoise (>=6.12.0,<7.0.0)

  • +
+
+
+

Documentation env

+
    +
  • Sphinx (==8.1.3)

  • +
  • sphinx_rtd_theme (==3.1.0)

  • +
+
+
+
+

Dependency manager and installation

+
+

Pip

+

First, create the virtual environment : +.. code:

+
py -3.10 -m venv env
+
+
+

Then, activate the virtual env :

+
    +
  • in Git Bash on Windows or on macOS / Linux

  • +
+
source env/bin/activate
+
+
+
    +
  • on Windows

  • +
+
env\Scripts\activate
+
+
+

To install dependencies, type :

+
pip install -r requirements.txt
+
+
+
+
+

Uv

+

UV is an environment and dependencies manager.

+

To install environment and dependencies, type :

+
uv sync
+
+
+

UV will use the .toml file to know which Python version and dependencies to install.

+
+
+

Poetry

+

POETRY is an environment and dependencies manager.

+

First, install the virtual environment :

+
py -3.10 -m venv env
+
+
+

Then, activate the virtual env :

+
poetry env activate
+
+
+

To install dependencies, type :

+
poetry install
+
+
+

POETRY will use the .toml file to know which dependencies to install.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/build/html/links.html b/docs/build/html/links.html new file mode 100644 index 0000000000..3b3f307b60 --- /dev/null +++ b/docs/build/html/links.html @@ -0,0 +1,129 @@ + + + + + + + + + Links — Orange County Lettings 1.0 documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/build/html/objects.inv b/docs/build/html/objects.inv new file mode 100644 index 0000000000..0fefe36389 Binary files /dev/null and b/docs/build/html/objects.inv differ diff --git a/docs/build/html/programing_interface.html b/docs/build/html/programing_interface.html new file mode 100644 index 0000000000..8a43f11821 --- /dev/null +++ b/docs/build/html/programing_interface.html @@ -0,0 +1,243 @@ + + + + + + + + + Programing Interface description — Orange County Lettings 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Programing Interface description

+
+

URL routes

+

Here are all the routes for each Django app :

+
+

oc_lettings_site app

+ + + + + + + + + + + + + + + + + + + + +

Route

Description

/

Home page

/lettings/

Lettings list

/profiles/

Profiles list

/admin/

Admin page

+
+
+

lettings app

+ + + + + + + + + + + + + + +

Route

Description

/lettings/

Lettings index page

/lettings/<int:letting_id>/

Letting detail

+
+
+

profiles app

+ + + + + + + + + + + + + + +

Route

Description

/profiles/

Profiles index page

/profiles/<int:profile_id>/

Profile detail

+
+
+
+

Django views

+

Each app has its views.py module according to the routes previously described.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Django App

View

Template rendered

oc_lettings_site

index

index.html

lettings

index

lettings/index.html

lettings

lettings

lettings/letting.html

profiles

index

profiles/index.html

profiles

profiles

lettings/profile.html

+

In addition, in each application view, we check if an error has occurred (404 or 500) and, if an error occurs, the view renders an error template.

+
+
+

Templates rendering

+

Each view renders templates with Django render method.

+

Each Django app has a template folder with one template for each view previously described and one for each possible error.

+
+
+

Database interaction

+

In all views, database access is achieved through an SQL query applied on the required model. The primary use case +involves calling the methods ‘objects.all()’ or objects.get() as shown below for example :

+
lettings_list = Letting.objects.all()
+
+
+
profile = Profile.objects.get(user__username=username)
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/build/html/quality.html b/docs/build/html/quality.html new file mode 100644 index 0000000000..8dfcc19c8d --- /dev/null +++ b/docs/build/html/quality.html @@ -0,0 +1,207 @@ + + + + + + + + + Quality — Orange County Lettings 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Quality

+
+

Testing

+

This Django project uses the Pytest library for testing the application.

+

Each Django app has a testing package with commonly 3 testing modules (for models, urls, and views) and a test fixture module conftest.py.

+

We guarantee 100% test coverage for the Orange County Lettings application in its new version.

+_images/cov_report_1_screenshot.png +
+

Environment configuration

+

In the env virtual environment, you must install Pytest library if not already installed as explained in the installation section.

+
pip install pytest
+
+
+
poetry add pytest
+
+
+
uv add pytest
+
+
+
+
+

Tests coverage

+

To complete the test process, the pytest-cov library is used to generate a coverage report.

+

To generate another report, you must install pytest-cov before. +Please use the same procedure as above for pytest.

+
+
+

Tests local execution

+

To generate another test process in your terminal, please type the line below :

+
pytest -v --cov=lettings --cov=profiles --cov=oc_lettings_site --cov-report=html:cov_html
+
+
+
+
+

Automatic tests execution

+

The Orange County Lettings application uses a CI/CD pipeline (detailed in deployment section) that automatically runs tests during the continuous integration task.

+

This pipeline uses a setup.cfg file containing the test command that generates a new report after each commit and push to your Git platform.

+
+
+
+

Linting

+

This Django project uses the Flake 8 linter that ensures you to implement an always-standardized code and according to the PEP 8 convention.

+
+

Environment configuration

+

In the env virtual environment, you must install Flake 8 library if not already installed as explained in the installation section.

+
pip install flake8
+
+
+
poetry add flake8
+
+
+
uv add flake8
+
+
+
+
+

Flake 8 report

+

To complete the flake 8 process by adding an HTML report, the flake8-html library is used to.

+

To generate another report, you must install flake8-html before. +Please use the same procedure as above for flake8.

+
+
+

Flake 8 local execution

+

To generate another flake 8 linting process in your terminal, please type the line below :

+
flake8 --format=html --htmldir=flake8-report --max-line-length=119 --extend-exclude="env/, env-docs/"
+
+
+
+
+

Automatic linting

+

The Orange County Lettings application uses a CI/CD pipeline (detailed in deployment section) that automatically runs Flake 8 linter during the continuous integration task.

+

This pipeline uses a setup.cfg file containing the Flake 8 command to generate a new report after each commit and push to your Git platform.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/build/html/quick_start.html b/docs/build/html/quick_start.html new file mode 100644 index 0000000000..f403c5a465 --- /dev/null +++ b/docs/build/html/quick_start.html @@ -0,0 +1,199 @@ + + + + + + + + + Quick start guide — Orange County Lettings 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Quick start guide

+
+

Launching server

+
+

Local server

+

Please follow the steps as below :

+
    +
  • Open a terminal

  • +
  • Go to project folder - example :

    +
    +
    cd oc_lettings_site
    +
    +
    +
    +
  • +
  • Activate the virtual environment as described previously

  • +
  • Create environment variables (to avoid to add raw Sentry key into the code) :

    +
      +
    • With Power Shell :

      +
      $env:SENTRY_KEY = "your_key"
      +
      +
      +
    • +
    • With Git Bash :

      +
      export SENTRY_KEY = "your_key"
      +
      +
      +
    • +
    +
  • +
  • Launch the local server by typing the command :

    +
    +
    python manage.py runserver
    +
    +
    +
    +
  • +
+
+
+

Web server

+

Please follow the procedure described in deployment section regarding the GitHub Actions or Gitlab CI/CD workflow, Docker containerization +and automatic deployment.

+
+
+
+

Launching the APP

+

Please follow the steps as below :

+
    +
  • With local server, open a web browser and type the urls :

    +
    +
    http://127.0.0.1:8000/
    +
    +
    +
    http://127.0.0.1:8000/admin
    +
    +
    +

    for the admin panel (username: admin, password: given in the project technical specifications)

    +
    +
  • +
  • With web server (after deployment), open a web browser and type the url :

    +
    +
      +
    • Your Heroku app url given in the Heroku dashboard, for example the url below :

      +
      +
      +
    • +
    +
    +
  • +
+

You also need to specify all required environment variables used by the application in your Heroku app. +Please follow the procedure detailed in the Heroku sub-section in deployment section.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/build/html/search.html b/docs/build/html/search.html new file mode 100644 index 0000000000..fea0ef27c2 --- /dev/null +++ b/docs/build/html/search.html @@ -0,0 +1,129 @@ + + + + + + + + Search — Orange County Lettings 1.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + + + +
+ +
+ +
+
+ +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/docs/build/html/searchindex.js b/docs/build/html/searchindex.js new file mode 100644 index 0000000000..9b9c4a7b14 --- /dev/null +++ b/docs/build/html/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {"ALLOWED_HOSTS errors": [[1, "allowed-hosts-errors"]], "Add compression and caching support": [[1, "add-compression-and-caching-support"]], "App env": [[5, "app-env"]], "Architecture": [[2, "architecture"]], "Automatic linting": [[8, "automatic-linting"]], "Automatic tests execution": [[8, "automatic-tests-execution"]], "Browsing lettings": [[11, "browsing-lettings"]], "By Nicolas MARIE - June 2026 - v1.0": [[4, "by-nicolas-marie-june-2026-v1-0"]], "CD pipeline": [[1, "cd-pipeline"]], "CI pipeline": [[1, "ci-pipeline"]], "CI/CD pipeline": [[1, "ci-cd-pipeline"]], "Configuration": [[1, "configuration"]], "Database admin page": [[0, "database-admin-page"]], "Database interaction": [[7, "database-interaction"]], "Database structure and models": [[0, null]], "Dependency manager and installation": [[5, "dependency-manager-and-installation"]], "Deployment": [[1, null]], "Description": [[2, null]], "Django settings": [[1, "django-settings"]], "Django views": [[7, "django-views"]], "Docker build fails": [[1, "docker-build-fails"]], "Docker containerization": [[1, "docker-containerization"]], "Documentation": [[3, null]], "Documentation editing": [[3, "documentation-editing"]], "Documentation env": [[5, "documentation-env"]], "Documentation local building": [[3, "documentation-local-building"]], "Documentation project initialization": [[3, "documentation-project-initialization"]], "Enable WhiteNoise": [[1, "enable-whitenoise"]], "Environment configuration": [[3, "environment-configuration"], [8, "environment-configuration"], [8, "id1"]], "Environment variables": [[1, "environment-variables"]], "Example of main use cases": [[11, "example-of-main-use-cases"]], "Flake 8 local execution": [[8, "flake-8-local-execution"]], "Flake 8 report": [[8, "flake-8-report"]], "GitHub": [[1, "github"], [1, "id1"]], "Gitlab": [[1, "gitlab"], [1, "id2"]], "Heroku": [[1, "heroku"]], "Heroku application crashes immediately": [[1, "heroku-application-crashes-immediately"]], "Home page": [[11, "home-page"]], "Installation": [[5, null]], "Launching server": [[9, "launching-server"]], "Launching the APP": [[9, "launching-the-app"]], "Letting 404 error page": [[11, "letting-404-error-page"]], "Letting details page": [[11, "letting-details-page"]], "Lettings list page": [[11, "lettings-list-page"]], "Lettings models": [[0, "lettings-models"]], "Libraries": [[5, "libraries"]], "Links": [[6, null]], "Linting": [[8, "linting"]], "Local server": [[9, "local-server"]], "Modular architecture": [[2, "modular-architecture"]], "Monitoring with Sentry": [[1, "monitoring-with-sentry"]], "Oc_lettings_site models": [[0, "oc-lettings-site-models"]], "Overview": [[2, "overview"]], "Pip": [[5, "pip"]], "Poetry": [[5, "poetry"]], "Profile details page": [[11, "profile-details-page"]], "Profiles list page": [[11, "profiles-list-page"]], "Profiles models": [[0, "profiles-models"]], "Programing Interface description": [[7, null]], "Python version": [[5, "python-version"]], "Quality": [[8, null]], "Quick overview": [[11, "quick-overview"]], "Quick start guide": [[9, null]], "ReadTheDocs configuration": [[3, "readthedocs-configuration"]], "ReadTheDocs documentation access": [[3, "readthedocs-documentation-access"]], "Required secrets/variables": [[1, "required-secrets-variables"]], "Returning to home page": [[11, "returning-to-home-page"]], "Secrets/variables": [[1, "secrets-variables"]], "Stack": [[10, null]], "Static files and WhiteNoise": [[1, "static-files-and-whitenoise"]], "Static files not loading": [[1, "static-files-not-loading"]], "Summary": [[2, "summary"]], "Templates rendering": [[7, "templates-rendering"]], "Testing": [[8, "testing"]], "Tests coverage": [[8, "tests-coverage"]], "Tests local execution": [[8, "tests-local-execution"]], "Troubleshooting": [[1, "troubleshooting"]], "URL routes": [[7, "url-routes"]], "Use cases": [[11, "use-cases"]], "User guide with use cases": [[11, null]], "Uv": [[5, "uv"]], "Viewing profiles": [[11, "viewing-profiles"]], "Virtual environments": [[5, "virtual-environments"]], "Web server": [[9, "web-server"]], "Welcome to Orange County Lettings documentation": [[4, null]], "lettings app": [[7, "lettings-app"]], "oc_lettings_site app": [[7, "oc-lettings-site-app"]], "profiles app": [[7, "profiles-app"]]}, "docnames": ["db_structure_and_models", "deployment", "description", "documentation", "index", "installation", "links", "programing_interface", "quality", "quick_start", "stack", "use_cases"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["db_structure_and_models.rst", "deployment.rst", "description.rst", "documentation.rst", "index.rst", "installation.rst", "links.rst", "programing_interface.rst", "quality.rst", "quick_start.rst", "stack.rst", "use_cases.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"": [1, 2], "0": [1, 5, 9], "1": [0, 1, 5, 9], "10": [5, 10], "100": 8, "119": 8, "12": 5, "127": [1, 9], "17": 5, "2": [0, 5], "256": [], "26": 5, "27": 5, "3": [0, 5, 8, 10], "4": 5, "400": 1, "404": 7, "5": 0, "500": 7, "6": 5, "60": 5, "7": 5, "8": 5, "8000": 9, "9": 5, "A": 1, "For": [1, 5], "If": [1, 3], "In": [0, 1, 3, 5, 7, 8, 11], "It": 3, "No": 0, "One": 5, "THE": 1, "The": [0, 1, 2, 3, 5, 7, 8, 10, 11], "Then": [1, 3, 5], "To": [1, 3, 5, 8], "With": 9, "_static": [], "abov": [1, 8], "access": [4, 7, 11], "accord": [1, 7, 8], "account": [1, 3], "achiev": 7, "action": [1, 9], "activ": [5, 9], "ad": [1, 8], "add": [3, 8, 9], "addit": [2, 7], "address": [0, 11], "admin": [4, 7, 9], "after": [1, 3, 8, 9], "again": 1, "all": [0, 1, 2, 3, 7, 9, 11], "allow": [2, 11], "alreadi": [1, 3, 8], "also": 9, "alwai": 8, "an": [0, 1, 3, 5, 7, 8], "anoth": [1, 3, 8], "api": 1, "app": [0, 1, 2, 4, 8, 10], "appli": 7, "applic": [0, 2, 5, 6, 7, 8, 9, 10, 11], "ar": [0, 1, 3, 5, 7], "architectur": 4, "aris": 1, "attribut": [0, 1], "automat": [3, 9], "avail": [0, 1, 2, 11], "avoid": 9, "backend": 1, "bad": 1, "base_dir": 1, "bash": [5, 9], "bat": 3, "been": [0, 2], "befor": [1, 8], "below": [0, 1, 3, 5, 6, 7, 8, 9, 10], "bin": 5, "bind": 1, "both": 5, "bottom": 1, "branch": 1, "browser": 9, "build": 4, "built": 1, "button": [1, 3, 11], "call": 7, "can": [1, 3, 6, 11], "cascad": [], "case": [4, 7], "caus": 1, "cd": [2, 4, 8, 9], "cfg": 8, "charfield": 0, "check": [1, 3, 6, 7], "checkout": 1, "choos": 3, "ci": [2, 4, 8, 9], "citi": 0, "cli": 1, "click": [1, 3, 11], "code": [0, 1, 2, 5, 8, 9, 10], "collectstat": 1, "com": 1, "come": 1, "command": [1, 3, 8, 9], "commit": [1, 3, 8], "common": 1, "commonli": 8, "complet": [1, 8], "compressedmanifeststaticfilesstorag": 1, "config": 1, "configur": 4, "confirm": 1, "conftest": 8, "connect": 3, "contain": [1, 8], "container": [4, 9, 10], "content": 3, "continu": [1, 8], "convent": 8, "copi": 1, "correctli": 1, "counti": [2, 6, 8, 10, 11], "country_iso_cod": 0, "cov": [5, 6, 8], "cov_html": 8, "coverag": 1, "creat": [1, 3, 5, 9], "creation": 2, "current": [0, 2], "dashboard": [1, 9], "databas": [4, 10], "database_tables_1_screenshot": [], "debt": 2, "debug": 1, "default": 3, "defin": 1, "deliveri": 1, "depend": [1, 4], "deploi": 1, "deploy": [2, 4, 8, 9, 10], "describ": [1, 7, 9], "descript": 4, "design": 2, "detail": [1, 6, 7, 8, 9], "detect": 3, "dev": 1, "develop": [2, 10], "differ": 0, "directli": 1, "displai": 11, "django": [0, 2, 4, 5, 6, 8], "django_allowed_host": 1, "do": 1, "doc": [2, 3, 5, 8], "docker": [4, 6, 9, 10], "docker_password": 1, "docker_usernam": 1, "dockerfil": 1, "document": [2, 10], "domain": 1, "don": [1, 3], "done": [1, 2], "dotenv": 5, "down": 1, "dropdown": 1, "dure": 8, "each": [0, 1, 2, 3, 6, 7, 8], "earlier": 1, "edit": [1, 4], "end": 1, "ensur": [1, 8], "enter": 1, "entri": [0, 3], "env": [3, 8, 9], "environ": [4, 9], "error": [2, 7], "estat": 2, "exampl": [1, 4, 7, 9], "except": [0, 1, 10], "exclud": 8, "execut": 1, "exist": [1, 3], "expand": [1, 2], "explain": [3, 8], "export": 9, "extend": 8, "fals": 1, "familiar": 1, "favorite_c": 0, "file": [3, 4, 5, 8], "final": [1, 2, 3], "finalis": 3, "find": [1, 6], "first": [0, 3, 5], "fix": 1, "fixtur": 8, "flake8": [5, 6, 8], "flexibl": 2, "focu": 1, "folder": [2, 3, 7, 9], "follow": [1, 3, 9], "forev": 1, "format": [1, 8], "from": [0, 1, 2, 11], "front": 1, "gener": [3, 8], "get": [1, 7], "git": [1, 2, 3, 5, 8, 9], "github": [3, 9], "gitlab": [3, 9], "given": 9, "go": [1, 9], "goal": [], "guarante": 8, "guid": 4, "gunicorn": [1, 5, 10], "ha": [0, 1, 2, 7, 8, 11], "hash": 1, "have": [1, 2, 3], "help": 3, "hera": 1, "here": [0, 1, 3, 7, 11], "heroku": [4, 6, 9, 10], "heroku_api_kei": 1, "heroku_user_email": 1, "herokuapp": 1, "home": 7, "host": 1, "how": 1, "html": [2, 3, 5, 6, 7, 8], "htmldir": 8, "http": [1, 9], "i": [0, 1, 2, 5, 7, 8, 10, 11], "id": 11, "ident": 1, "imag": [1, 10], "implement": [0, 1, 8], "import": [1, 3], "improv": 2, "includ": 3, "index": [3, 7], "indic": 1, "inform": 11, "initi": [1, 4], "instal": [1, 3, 4, 8, 10], "int": 7, "integr": [1, 8], "interact": 4, "interfac": [1, 4], "invalid": 1, "involv": 7, "issu": 1, "its": [0, 1, 2, 7, 8], "job": 1, "just": [1, 2], "kei": [1, 9], "kind": 1, "know": [1, 5], "languag": 3, "latter": 1, "launch": 4, "left": 1, "length": 8, "less": 11, "let": [2, 6, 8, 10], "letting_id": 7, "lettings_list": 7, "librari": [1, 3, 4, 6, 8, 10], "line": [1, 8], "link": [1, 3, 4, 11], "lint": 4, "linter": [1, 8], "linux": 5, "list": [1, 7], "ll": 1, "local": 4, "localhost": 1, "log": [1, 10], "login": 1, "m": 5, "maco": 5, "main": [0, 1, 4], "maintain": 2, "make": 3, "manag": [4, 9], "master": 1, "max": 8, "max_length": [], "md": 3, "menu": 1, "method": 7, "middlewar": 1, "miss": 1, "model": [2, 4, 7, 8], "modif": 3, "modul": [0, 2, 7, 8], "monitor": [2, 4, 10], "monolith": 2, "more": [6, 11], "must": [1, 2, 3, 8], "my": 1, "name": 1, "namespac": 0, "need": [1, 3, 9], "new": [0, 1, 2, 3, 5, 8, 10], "next": 1, "non": 1, "now": 1, "number": 0, "object": [0, 7], "oc_letting_sit": 2, "oc_lettings_app": 0, "oc_lettings_sit": [1, 4, 8, 9], "occur": 7, "old": 0, "on_delet": [], "onc": 1, "one": [0, 1, 3, 7], "onetoonefield": 0, "onli": 0, "onlin": 3, "open": [1, 9, 11], "optim": 2, "option": 3, "orang": [2, 6, 8, 10, 11], "order": [1, 3], "other": 1, "otherwis": 1, "overview": 4, "own": 2, "packag": 8, "page": [4, 7], "panel": 9, "password": 9, "pep": 8, "perform": 1, "pip": [3, 8], "pipelin": [2, 4, 8], "place": 1, "platform": [1, 2, 3, 8], "pleas": [1, 3, 8, 9], "png": [], "poetri": [3, 8], "point": [0, 3], "port": 1, "positiveintegerfield": 0, "possibl": [1, 7], "power": 9, "previou": [1, 2], "previous": [0, 7, 9], "primari": 7, "procedur": [8, 9, 11], "process": [1, 8], "product": 1, "profil": [1, 2, 4, 8], "profile_id": 7, "program": 4, "project": [1, 2, 4, 8, 9], "provid": [1, 3], "push": [1, 3, 8], "py": [0, 1, 5, 7, 8, 9], "pytest": [1, 5, 6, 8, 10], "python": [4, 9, 10], "qualiti": [1, 4], "queri": 7, "quick": 4, "quickstart": 3, "r": 5, "rais": [], "rapidli": 2, "raw": 9, "re": 1, "read": 2, "readm": 3, "readthedoc": [4, 5, 10], "real": 2, "rebuild": 3, "redesign": 2, "reduc": 2, "reduct": 2, "regard": [0, 1, 9], "regist": [1, 2, 11], "relat": 11, "releas": [1, 2, 3], "render": 4, "rental": [2, 11], "reorgan": 2, "report": 1, "repositori": [1, 2, 3], "represent": 0, "requir": [3, 5, 7, 9], "restructuredtext": 3, "retriev": 1, "reveal": 1, "review": 1, "rigor": 1, "root": 1, "rout": 4, "rst": 3, "run": [1, 8], "runserv": 9, "safe": 1, "same": [5, 8, 11], "save": 1, "scalabl": 2, "scale": 2, "screenshot": 0, "script": 5, "scroll": 1, "sdk": [1, 5, 6], "secret": 4, "secret_kei": 1, "section": [1, 3, 8, 9, 10], "sector": 2, "secur": 1, "securitymiddlewar": 1, "see": [1, 3, 10], "select": 11, "sentri": [2, 4, 5, 6, 9, 10], "sentry_kei": [1, 9], "separ": 2, "serv": 0, "server": 4, "servic": 1, "set": [0, 4], "setup": 8, "sever": 2, "shell": 9, "should": 1, "shown": [1, 7], "sidebar": 1, "six": 5, "so": 1, "solut": 1, "some": [1, 10], "sourc": 5, "specif": [1, 2, 9], "specifi": [1, 9], "sphinx": [2, 3, 5], "sphinx_rtd_them": 5, "split": 0, "sql": 7, "sqlite": 10, "stack": 4, "standard": 8, "start": [1, 4], "startup": 2, "state": [0, 2], "static": 4, "static_root": 1, "staticfil": 1, "step": [1, 3, 9], "storag": 1, "street": 0, "structur": [1, 4], "sub": [1, 9], "subsect": 1, "success": 1, "successfulli": 1, "summari": 4, "sync": 5, "synchron": 3, "t": [1, 3], "task": 8, "team": 2, "technic": [2, 9], "templat": [2, 4], "termin": [3, 8, 9], "test": [1, 2, 4, 10], "thei": 1, "them": 1, "therefor": 1, "thi": [0, 1, 2, 5, 8], "those": 1, "through": 7, "titl": 0, "toml": 5, "track": 2, "troubleshoot": 4, "true": 1, "try": 1, "two": 3, "txt": [1, 5], "type": [0, 3, 5, 8, 9], "uniqu": 1, "unit": 2, "url": [0, 1, 2, 3, 4, 8, 9], "us": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "user": [0, 2, 4], "user__usernam": 7, "usernam": [7, 9, 11], "usual": 1, "uv": [3, 8], "v": [0, 8], "valid": [1, 3], "valu": 1, "var": 1, "variabl": [4, 9], "variou": 2, "venv": 5, "veri": 1, "version": [0, 2, 4, 8, 10], "via": 2, "view": [0, 1, 2, 4, 8], "virtual": [3, 4, 8, 9], "wa": 1, "want": 3, "we": [1, 2, 5, 7, 8], "web": [0, 1, 2, 10], "websit": [1, 3], "well": 1, "what": [1, 2], "when": 1, "which": [1, 5], "while": [1, 3], "whitenois": [4, 5, 6], "whitenoisemiddlewar": 1, "window": 5, "worflow": [], "work": 1, "workflow": [1, 9], "wsgi": 1, "yml": 1, "you": [1, 3, 6, 8, 9], "your": [1, 3, 8, 9], "your_kei": 9, "zip_cod": 0}, "titles": ["Database structure and models", "Deployment", "Description", "Documentation", "Welcome to Orange County Lettings documentation", "Installation", "Links", "Programing Interface description", "Quality", "Quick start guide", "Stack", "User guide with use cases"], "titleterms": {"0": 4, "2026": 4, "404": 11, "8": 8, "By": 4, "access": 3, "add": 1, "admin": 0, "allowed_host": 1, "app": [5, 7, 9], "applic": 1, "architectur": 2, "automat": 8, "brows": 11, "build": [1, 3], "cach": 1, "case": 11, "cd": 1, "ci": 1, "compress": 1, "configur": [1, 3, 8], "container": 1, "counti": 4, "coverag": 8, "crash": 1, "databas": [0, 7], "depend": 5, "deploy": 1, "descript": [2, 7], "detail": 11, "django": [1, 7], "docker": 1, "document": [3, 4, 5], "edit": 3, "enabl": 1, "env": 5, "environ": [1, 3, 5, 8], "error": [1, 11], "exampl": 11, "execut": 8, "fail": 1, "file": 1, "flake": 8, "github": 1, "gitlab": 1, "guid": [9, 11], "heroku": 1, "home": 11, "immedi": 1, "initi": 3, "instal": 5, "interact": 7, "interfac": 7, "june": 4, "launch": 9, "let": [0, 4, 7, 11], "librari": 5, "link": 6, "lint": 8, "list": 11, "load": 1, "local": [3, 8, 9], "main": 11, "manag": 5, "mari": 4, "model": 0, "modular": 2, "monitor": 1, "nicola": 4, "oc_lettings_sit": [0, 7], "orang": 4, "overview": [2, 11], "page": [0, 11], "pip": 5, "pipelin": 1, "poetri": 5, "profil": [0, 7, 11], "program": 7, "project": 3, "python": 5, "qualiti": 8, "quick": [9, 11], "readthedoc": 3, "render": 7, "report": 8, "requir": 1, "return": 11, "rout": 7, "secret": 1, "sentri": 1, "server": 9, "set": 1, "stack": 10, "start": 9, "static": 1, "structur": 0, "summari": 2, "support": 1, "templat": 7, "test": 8, "troubleshoot": 1, "url": 7, "us": 11, "user": 11, "uv": 5, "v1": 4, "variabl": 1, "version": 5, "view": [7, 11], "virtual": 5, "web": 9, "welcom": 4, "whitenois": 1}}) \ No newline at end of file diff --git a/docs/build/html/stack.html b/docs/build/html/stack.html new file mode 100644 index 0000000000..948f9b77f7 --- /dev/null +++ b/docs/build/html/stack.html @@ -0,0 +1,127 @@ + + + + + + + + + Stack — Orange County Lettings 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Stack

+

The stack used for developing Orange County Lettings new version is as below :

+
    +
  • Python 3.10 and some libraries (see installation section) for app code

  • +
  • Pytest library for testing

  • +
  • SQLite database

  • +
  • Docker for image containerization

  • +
  • Heroku (and Gunicorn library) for web deployment

  • +
  • Sentry for logs and exceptions monitoring

  • +
  • ReadTheDocs for the application documentation

  • +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/build/html/use_cases.html b/docs/build/html/use_cases.html new file mode 100644 index 0000000000..c37c286739 --- /dev/null +++ b/docs/build/html/use_cases.html @@ -0,0 +1,197 @@ + + + + + + + + + User guide with use cases — Orange County Lettings 1.0 documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

User guide with use cases

+
+

Quick overview

+

Orange County Lettings allows the user to browse available rentals and all the registered profiles.

+

The user has access to lettings and profiles lists, and can display a letting or a profile detail by id or username.

+
+
+

Example of main use cases

+
+

Home page

+_images/home_page_screenshot.png +
+
+

Lettings list page

+_images/lettings_list_screenshot.png +
+
+

Letting details page

+_images/letting_details_screenshot.png +
+
+

Profiles list page

+_images/profiles_list_screenshot.png +
+
+

Profile details page

+_images/profile_details_screenshot.png +
+
+

Letting 404 error page

+_images/letting_404_error_screenshot.png +
+
+
+

Use cases

+
+

Browsing lettings

+

Here is the procedure :

+
    +
  • Open the home page

  • +
  • Click on the “Letting” link

  • +
  • Select a letting from the list

  • +
  • The detail page displays the address and related information

  • +
+
+
+

Viewing profiles

+

The procedure is more or less the same :

+
    +
  • Open the Profiles page.

  • +
  • Select a profile from the list.

  • +
  • The application displays the user profile details.

  • +
+
+
+

Returning to home page

+

Here is the procedure :

+
    +
  • In the lettings page or profiles page, click on the “Home” button

  • +
  • The application displays the home page

  • +
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000000..dc1312ab09 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/screenshots/cov_report_1_screenshot.png b/docs/screenshots/cov_report_1_screenshot.png new file mode 100644 index 0000000000..8da9ced1d2 Binary files /dev/null and b/docs/screenshots/cov_report_1_screenshot.png differ diff --git a/docs/screenshots/cov_report_2_screenshot.png b/docs/screenshots/cov_report_2_screenshot.png new file mode 100644 index 0000000000..58c9a2c22e Binary files /dev/null and b/docs/screenshots/cov_report_2_screenshot.png differ diff --git a/docs/screenshots/flake8_report_screenshot.png b/docs/screenshots/flake8_report_screenshot.png new file mode 100644 index 0000000000..3736cee381 Binary files /dev/null and b/docs/screenshots/flake8_report_screenshot.png differ diff --git a/docs/screenshots/home_page_screenshot.png b/docs/screenshots/home_page_screenshot.png new file mode 100644 index 0000000000..798f0950ee Binary files /dev/null and b/docs/screenshots/home_page_screenshot.png differ diff --git a/docs/screenshots/letting_details_screenshot.png b/docs/screenshots/letting_details_screenshot.png new file mode 100644 index 0000000000..ab0dbd83bb Binary files /dev/null and b/docs/screenshots/letting_details_screenshot.png differ diff --git a/docs/screenshots/lettings_list_screenshot.png b/docs/screenshots/lettings_list_screenshot.png new file mode 100644 index 0000000000..21f41d766f Binary files /dev/null and b/docs/screenshots/lettings_list_screenshot.png differ diff --git a/docs/screenshots/profile_details_screenshot.png b/docs/screenshots/profile_details_screenshot.png new file mode 100644 index 0000000000..57ca31328d Binary files /dev/null and b/docs/screenshots/profile_details_screenshot.png differ diff --git a/docs/screenshots/profiles_list_screenshot.png b/docs/screenshots/profiles_list_screenshot.png new file mode 100644 index 0000000000..835b72dbdd Binary files /dev/null and b/docs/screenshots/profiles_list_screenshot.png differ diff --git a/docs/screenshots/structure_screenshot.png b/docs/screenshots/structure_screenshot.png new file mode 100644 index 0000000000..378ffb758e Binary files /dev/null and b/docs/screenshots/structure_screenshot.png differ diff --git a/docs/source/_static/cov_report_1_screenshot.png b/docs/source/_static/cov_report_1_screenshot.png new file mode 100644 index 0000000000..8da9ced1d2 Binary files /dev/null and b/docs/source/_static/cov_report_1_screenshot.png differ diff --git a/docs/source/_static/database_tables_1_screenshot.png b/docs/source/_static/database_tables_1_screenshot.png new file mode 100644 index 0000000000..0efa8af694 Binary files /dev/null and b/docs/source/_static/database_tables_1_screenshot.png differ diff --git a/docs/source/_static/home_page_screenshot.png b/docs/source/_static/home_page_screenshot.png new file mode 100644 index 0000000000..798f0950ee Binary files /dev/null and b/docs/source/_static/home_page_screenshot.png differ diff --git a/docs/source/_static/letting_404_error_screenshot.png b/docs/source/_static/letting_404_error_screenshot.png new file mode 100644 index 0000000000..686e8aa997 Binary files /dev/null and b/docs/source/_static/letting_404_error_screenshot.png differ diff --git a/docs/source/_static/letting_details_screenshot.png b/docs/source/_static/letting_details_screenshot.png new file mode 100644 index 0000000000..ab0dbd83bb Binary files /dev/null and b/docs/source/_static/letting_details_screenshot.png differ diff --git a/docs/source/_static/lettings_list_screenshot.png b/docs/source/_static/lettings_list_screenshot.png new file mode 100644 index 0000000000..21f41d766f Binary files /dev/null and b/docs/source/_static/lettings_list_screenshot.png differ diff --git a/docs/source/_static/logo.png b/docs/source/_static/logo.png new file mode 100644 index 0000000000..47ecb49b49 Binary files /dev/null and b/docs/source/_static/logo.png differ diff --git a/docs/source/_static/profile_details_screenshot.png b/docs/source/_static/profile_details_screenshot.png new file mode 100644 index 0000000000..57ca31328d Binary files /dev/null and b/docs/source/_static/profile_details_screenshot.png differ diff --git a/docs/source/_static/profiles_list_screenshot.png b/docs/source/_static/profiles_list_screenshot.png new file mode 100644 index 0000000000..835b72dbdd Binary files /dev/null and b/docs/source/_static/profiles_list_screenshot.png differ diff --git a/docs/source/_static/structure_screenshot.png b/docs/source/_static/structure_screenshot.png new file mode 100644 index 0000000000..378ffb758e Binary files /dev/null and b/docs/source/_static/structure_screenshot.png differ diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000000..71648e27d9 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,28 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'Orange County Lettings' +copyright = '2026, Nicolas MARIE' +author = 'Nicolas MARIE' +release = '1.0' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [] + +templates_path = ['_templates'] +exclude_patterns = [] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'alabaster' +html_static_path = ['_static'] + +html_theme = "sphinx_rtd_theme" diff --git a/docs/source/db_structure_and_models.rst b/docs/source/db_structure_and_models.rst new file mode 100644 index 0000000000..a5533b545b --- /dev/null +++ b/docs/source/db_structure_and_models.rst @@ -0,0 +1,67 @@ +Database structure and models +============================= + +The code has been split in 3 different apps vs only one in the old version : + +* lettings +* oc_lettings_site +* profiles + +Each new app has 5 main modules as below : + +* admin.py for the representation in admin page +* apps.py for the app namespace +* models.py for the models used by the database (except for oc_lettings_app) +* urls.py for the urls +* views.py for the views + +Lettings models +--------------- +In the lettings models there are 2 objects with all attributes regarding the address and letting, previously implemented in the first version of the application : + +* Address object + +================ ==================== +Attribute Type +================ ==================== +number PositiveIntegerField +street CharField +city CharField +state CharField +zip_code PositiveIntegerField +country_iso_code CharField +================ ==================== + +* Letting object + +========= ======================== +Attribute Type +========= ======================== +title CharField +address OneToOneField to Address +========= ======================== + +Oc_lettings_site models +----------------------- +No models are currently available in this application in its new version. + +This Django app serves only as an entry point of the web application and for Django settings. + +Profiles models +--------------- +In the profiles models, there is 1 object with all attributes regarding the profiles previously implemented in the first version of the application : + +* Profile object + +============= ===================== +Attribute Type +============= ===================== +user OneToOneField to User +favorite_city CharField +============= ===================== + +Database admin page +------------------- +Here is a screenshot from the database admin page : + +.. image:: _static/database_tables_1_screenshot.png diff --git a/docs/source/deployment.rst b/docs/source/deployment.rst new file mode 100644 index 0000000000..52b1d823f0 --- /dev/null +++ b/docs/source/deployment.rst @@ -0,0 +1,266 @@ +Deployment +========== + +Django settings +--------------- +The Django application's `settings.py` file is used to configure the application regarding some very important +environment variables used by the CI/CD pipeline such as "DEBUG" and "DJANGO_ALLOWED_HOSTS" (the latter must be set on +your Heroku profile) and regarding the static files used by the application's front-end. + +Static files and WhiteNoise +--------------------------- + +You can find the detailed specific configuration of WhiteNoise library by checking the url below : `WhiteNoise `_ + +Configuration +^^^^^^^^^^^^^ +If you’re familiar with Django you’ll know what to do. If you’re just getting started with a new Django project then you’ll need add the following to the bottom of your settings.py file: + +.. code:: + + STATIC_ROOT = BASE_DIR / "staticfiles" + +Enable WhiteNoise +^^^^^^^^^^^^^^^^^ +The WhiteNoise library has to be installed before if not already done. +Please edit your `settings.py` file and add WhiteNoise to the MIDDLEWARE list. +The WhiteNoise middleware should be placed directly after the Django SecurityMiddleware (if you are using it) and before all other middleware: + +.. code:: + + MIDDLEWARE = [ + # ... + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + # ... + ] + +Add compression and caching support +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +WhiteNoise comes with a storage backend which compresses your files and hashes them to unique names, so they can safely be cached forever. To use it, set it as your staticfiles storage backend in your settings file: + +.. code:: + + STORAGES = { + # ... + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, + } + +Docker containerization +----------------------- +A Dockerfile in the repository root defines the process for building an image of the application and its dependencies +as a Docker container. Here are the main steps in this image building : + +* Copy the dependencies from the `requirements.txt` file and install them. + +* Retrieve the static files of the Django application. + +* Run Gunicorn (a library required for deployment on Heroku) on the specified application (for example, `oc_lettings_site.wsgi:application`), and the specified host and port. + +CI/CD pipeline +-------------- +GitHub Actions is the CI/CD solution provided by GitHub, while GitLab uses GitLab CI/CD pipelines. + +The `.yml` file structures the CI/CD pipelines and it is used by GitHub for creating GitHub Actions or used by Gitlab for creating CI/CD workflow. + +CI pipeline +^^^^^^^^^^^ +The continuous integration (CI) pipeline ensures the quality of the code. +The CI pipeline defined in the "ci" job runs as follows: + +* Checkout on the branch from which the push was performed + +* Installing the project's dependencies + +* Running linters (quality step) + +* Running tests with the `pytest` command (testing step) + +* Finally, loading the quality and test coverage reports + +CD pipeline +^^^^^^^^^^^ +The Continuous Delivery (CD) pipeline ensures the delivery and deployment of the app. +The CD pipeline defined in the "deploy" job works as follows: + +* Requires the successful execution of the previous continuous integration (CI) job (described earlier) on the master branch. + +* Checkout on the master branch. If the commit & push are performed from another branch, the CD pipeline is not executed. + +* The Docker image is initialized. + +* The Docker image is built and pushed (containerization process). + +* The Heroku command-line interface (CLI) is installed. + +* Deployment to Heroku. + +Secrets/variables +----------------- +In order to use correctly this Django web application, you must define some secrets in your Git platform secrets section. +Those secrets are used in the django.yml file that describes the CI/CD pipeline. + +GitHub +^^^^^^ + +Here's how you can add a secret on GitHub : + +* Go to your GitHub profile and open the project repository. + +* Click on "Settings", then on the "Secrets & Variables" section, and finally on the "Actions" button in the dropdown menu. + +* Next, click on "New Repository Secret" and enter the secret name and the secret value. Then confirm. The new secret is added to the repository. + +Gitlab +^^^^^^ + +Here's how you can add a secret variable on GitLab : + +* Go to your GitLab profile and open the project repository. + +* In the left sidebar, click on "Settings" and then on "CI/CD". + +* Expand the "Variables" section. + +* Click on "Add variable". + +* Enter the variable key (name) and the variable value, then click on "Add variable" to save it. + +* The new variable is now available in the GitLab CI/CD pipeline. + +Required secrets/variables +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here are all the required secrets/variables : + +* DOCKER_PASSWORD + +* DOCKER_USERNAME + +* HEROKU_API_KEY + +* HEROKU_USER_EMAIL + +* SENTRY_KEY + +When created, you can then use this secret with the variable `secrets` as shown below : + +.. code:: + + ${{ secrets. }} + +All secrets are used according to this format in the django.yml file which describes the CI/CD pipeline. + +The variable attribute name and the secret name has to be rigorously identical. + +Heroku +------ + +GitHub +^^^^^^ + +Once the GitHub actions are successfully completed, the application is deployed as a web service. Please check the +application's Heroku URL or click the "View application" button in your Heroku account to view the web application. + +If the GitHub actions failed, please review the logs on GitHub or in Heroku, fix the errors, and try the deployment +again. + +Gitlab +^^^^^^ + +Once the Gitlab CI/CD is successfully completed, the application is deployed as a web service. Please check the +application's Heroku URL or click the "View application" button in your Heroku account to view the web application. + +If the Gitlab CI/CD failed, please review the logs on the platform or in Heroku, fix the errors, and try the deployment +again. + +Environment variables +^^^^^^^^^^^^^^^^^^^^^ +The application needs some environment variables to work well. You must specify the variables into your Heroku app. +Please follow the steps below : + +* Go into your app in Heroku dashboard + +* Go to settings section + +* scroll down to "config vars" sub-section + +* click on the "Reveal Config Vars" button to reveal the variables if some are already existing + +* For each environment variable needed by the project, enter the "key" and the "value", and validate by clicking the "add' button + +Hera are all the variables regarding Django you need to provide : + +==================== ================================================================= +Key Value +==================== ================================================================= +DEBUG False (required for production deployment, True otherwise in dev) +DJANGO_ALLOWED_HOSTS localhost,127.0.0.1,.herokuapp.com +SECRET_KEY +==================== ================================================================= + +Monitoring with Sentry +---------------------- +In this project, the CI/CD pipeline uses the Sentry SDK for exceptions and logs monitoring. + +You will need a Sentry account to run the application correctly. Therefore, please register on the Sentry website +if you don't already have one. + +Another step: Before using the deployment pipeline, you must define a secret or variable into your Git platform for the Sentry key, required to link +the application to your Sentry account. Please see the "Secrets" subsection above. + +Troubleshooting +--------------- +In this section we focus on some common issues that arise in this kind of pipeline. + +Heroku application crashes immediately +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Here are the possible causes : + +* bad port + +* non-used port + +* bad Gunicorn configuration + + +Example : +If the application crashes immediately after deployment, ensure Gunicorn binds to the environment PORT variable: + +.. code:: bash + + gunicorn oc_lettings_site.wsgi:application --bind 0.0.0.0:$PORT + +ALLOWED_HOSTS errors +^^^^^^^^^^^^^^^^^^^^ +Example : +A HTTP 400 error usually indicates that the Heroku domain is missing from DJANGO_ALLOWED_HOSTS, environment variable +defines in Heroku app. + +.. code:: bash + + DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,my-app.herokuapp.com + +Static files not loading +^^^^^^^^^^^^^^^^^^^^^^^^ +Here are the possible causes : + +* collectstatic not implemented + +* WhiteNoise not used + +* STATIC_ROOT not defined in Django app settings.py file + +Docker build fails +^^^^^^^^^^^^^^^^^^ +Here are the possible causes : + +* secrets missing + +* invalid Heroku API key + +* bad Docker login + +* release failed diff --git a/docs/source/description.rst b/docs/source/description.rst new file mode 100644 index 0000000000..2cfcd961ff --- /dev/null +++ b/docs/source/description.rst @@ -0,0 +1,50 @@ + +Description +=========== +Orange County Lettings is a startup in the real estate rental sector. The startup is currently expanding rapidly in the +United States. +The Orange County Lettings teams developed the +`OC_Letting_Site `_ web application and the new scaled +version has just been released. + +Summary +------- + +The new version has been scaled using a modular architecture. + +What we have done : + +* Redesign of the modular architecture in the Git platform repository +* Reduction of various technical debts on the project +* Addition and deployment of a CI/CD pipeline +* Application monitoring and error tracking via Sentry +* Creation of the application's technical documentation using Read The Docs and Sphinx + +The application must : + +* allow the users to view available rentals and all the registered profiles. + +Architecture +------------ + +Overview +^^^^^^^^ +.. image:: _static/structure_screenshot.png + +Modular architecture +^^^^^^^^^^^^^^^^^^^^ +The architecture has been optimized by reducing the technical debts from the previous monolithic design. + +The code has been : + +* reorganized into several separate Django applications +* reorganized into application-specific HTML templates folders + +This optimization has improved the flexibility, maintainability, and scalability of the code. + +Finally, each app has its own : + +* views module +* urls module +* templates folder +* test folder with several test modules for models, views and urls diff --git a/docs/source/documentation.rst b/docs/source/documentation.rst new file mode 100644 index 0000000000..3a5d4e4d9a --- /dev/null +++ b/docs/source/documentation.rst @@ -0,0 +1,62 @@ +Documentation +============= + +Environment configuration +------------------------- +In the `env-docs` virtual environment, you must install Sphinx library if not already installed as explained in the :doc:`installation section `. + +.. code:: + + pip install sphinx + +.. code:: + + poetry add sphinx + +.. code:: + + uv add sphinx + +Documentation project initialization +------------------------------------ +Please create a "docs" folder or use one if existing and type in your terminal the command below to initialize the documentation : + +.. code:: + + sphinx-quickstart + +You can choose default options and validate. It will create all required files used by ReadTheDocs documentation. + +Documentation editing +--------------------- +You can use the index.rst as an entry point for the documentation content. Please use reStructuredText language in order to add content. +If you need help, you can check the url below to see `reStructuredText help documentation `_. + +Documentation local building +---------------------------- +To generate your documentation locally while editing content, you must type in your terminal the command below : + +.. code:: + + .\docs\make.bat html + +ReadTheDocs configuration +------------------------- +Here are the steps to follow if you want to generate another documentation in your account into the ReadTheDocs website : + +* First, you need to create an account on the ReadTheDocs website if you don't already have one. + +* Then create a new documentation project and link it to your Git platform : + * Click on "Import a repository" and then "Connect to GitHub/GitLab" + * Click on "+" button to finalise the importation + * The two accounts are synchronized + +* Configure the options of the ReadTheDocs projects + +* Configure the option for the automatic building and release after each push & commit on GitHub/Gitlab + +* Finally, ReadTheDocs will detect each modification and will rebuild the documentation online + +ReadTheDocs documentation access +-------------------------------- +ReadTheDocs provides an URL to your documentation online. You can include that URL in your repository `readme.md` file. diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000000..5d289d9824 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,29 @@ +.. Orange County Lettings documentation master file, created by + sphinx-quickstart on Tue May 26 16:37:54 2026. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +=============================================== +Welcome to Orange County Lettings documentation +=============================================== + +.. image:: _static/logo.png + +------------------------------------------------------------------- +By `Nicolas MARIE `_ - June 2026 - v1.0 +------------------------------------------------------------------- + +.. toctree:: + :maxdepth: 2 + + description + stack + installation + quick_start + db_structure_and_models + quality + programing_interface + use_cases + deployment + documentation + links diff --git a/docs/source/installation.rst b/docs/source/installation.rst new file mode 100644 index 0000000000..b648d8143f --- /dev/null +++ b/docs/source/installation.rst @@ -0,0 +1,119 @@ +Installation +============ + +Python version +-------------- + +For this new app version, the Python version is the same : Python 3.10 + +Virtual environments +-------------------- +In this Django application we use 2 virtual environments as below : + +* One for the application environment (env) + +* One for the ReadTheDocs documentation environment (env-docs) + +Libraries +--------- +The libraries used are for both environments : + +App env +^^^^^^^ +* django (==3.0) + +* flake8 (==3.7.0) + +* flake8-html (==0.4.3) + +* pytest (==9.0.3) + +* pytest-django (==4.12.0) + +* pytest-cov (==7.1.0) + +* six (==1.17.0) + +* sentry-sdk (>=2.60.0,<3.0.0) + +* python-dotenv (>=1.2.2,<2.0.0) + +* gunicorn (>=26.0.0,<27.0.0) + +* whitenoise (>=6.12.0,<7.0.0) + +Documentation env +^^^^^^^^^^^^^^^^^ +* Sphinx (==8.1.3) + +* sphinx_rtd_theme (==3.1.0) + +Dependency manager and installation +----------------------------------- + +Pip +^^^ + +First, create the virtual environment : +.. code:: + + py -3.10 -m venv env + +Then, activate the virtual env : + +* in Git Bash on Windows or on macOS / Linux + +.. code:: + + source env/bin/activate + + +* on Windows + +.. code:: + + env\Scripts\activate + +To install dependencies, type : + +.. code:: + + pip install -r requirements.txt + +Uv +^^ + +UV is an environment and dependencies manager. + +To install environment and dependencies, type : + +.. code:: + + uv sync + +UV will use the .toml file to know which Python version and dependencies to install. + +Poetry +^^^^^^ + +POETRY is an environment and dependencies manager. + +First, install the virtual environment : + +.. code:: + + py -3.10 -m venv env + +Then, activate the virtual env : + +.. code:: + + poetry env activate + +To install dependencies, type : + +.. code:: + + poetry install + +POETRY will use the .toml file to know which dependencies to install. diff --git a/docs/source/links.rst b/docs/source/links.rst new file mode 100644 index 0000000000..ab41e02e90 --- /dev/null +++ b/docs/source/links.rst @@ -0,0 +1,23 @@ +Links +===== +You can find more details on each library used in the Orange County Lettings application by checking the links below : + +* `Django `_ + +* `WhiteNoise `_ + +* `Flake8 `_ + +* `Flake8-html `_ + +* `Pytest `_ + +* `Pytest-cov `_ + +* `Pytest-django `_ + +* `Sentry-sdk `_ + +* `Docker `_ + +* `Heroku with Docker `_ diff --git a/docs/source/programing_interface.rst b/docs/source/programing_interface.rst new file mode 100644 index 0000000000..f48e4b8bc8 --- /dev/null +++ b/docs/source/programing_interface.rst @@ -0,0 +1,73 @@ +Programing Interface description +================================ + +URL routes +---------- +Here are all the routes for each Django app : + +`oc_lettings_site` app +^^^^^^^^^^^^^^^^^^^^^^ + +========== ============= +Route Description +========== ============= +/ Home page +/lettings/ Lettings list +/profiles/ Profiles list +/admin/ Admin page +========== ============= + +`lettings` app +^^^^^^^^^^^^^^ + +=========================== =================== +Route Description +=========================== =================== +/lettings/ Lettings index page +/lettings// Letting detail +=========================== =================== + +`profiles` app +^^^^^^^^^^^^^^ + +=========================== =================== +Route Description +=========================== =================== +/profiles/ Profiles index page +/profiles// Profile detail +=========================== =================== + +Django views +------------ +Each app has its views.py module according to the routes previously described. + +================ ======== ===================== +Django App View Template rendered +================ ======== ===================== +oc_lettings_site index index.html +lettings index lettings/index.html +lettings lettings lettings/letting.html +profiles index profiles/index.html +profiles profiles lettings/profile.html +================ ======== ===================== + +In addition, in each application view, we check if an error has occurred (404 or 500) and, if an error occurs, the view renders an error template. + +Templates rendering +------------------- +Each view renders templates with Django render method. + +Each Django app has a template folder with one template for each view previously described and one for each possible error. + +Database interaction +-------------------- +In all views, database access is achieved through an SQL query applied on the required model. The primary use case +involves calling the methods 'objects.all()' or `objects.get()` as shown below for example : + +.. code:: + + lettings_list = Letting.objects.all() + +.. code:: + + profile = Profile.objects.get(user__username=username) diff --git a/docs/source/quality.rst b/docs/source/quality.rst new file mode 100644 index 0000000000..eaf3fec098 --- /dev/null +++ b/docs/source/quality.rst @@ -0,0 +1,90 @@ +Quality +======= + +Testing +------- +This Django project uses the Pytest library for testing the application. + +Each Django app has a testing package with commonly 3 testing modules (for models, urls, and views) and a test fixture module `conftest.py`. + +We guarantee 100% test coverage for the Orange County Lettings application in its new version. + +.. image:: _static/cov_report_1_screenshot.png + +Environment configuration +^^^^^^^^^^^^^^^^^^^^^^^^^ +In the `env` virtual environment, you must install Pytest library if not already installed as explained in the :doc:`installation section `. + +.. code:: + + pip install pytest + +.. code:: + + poetry add pytest + +.. code:: + + uv add pytest + +Tests coverage +^^^^^^^^^^^^^^ +To complete the test process, the `pytest-cov` library is used to generate a coverage report. + +To generate another report, you must install `pytest-cov` before. +Please use the same procedure as above for `pytest`. + +Tests local execution +^^^^^^^^^^^^^^^^^^^^^ +To generate another test process in your terminal, please type the line below : + +.. code:: + + pytest -v --cov=lettings --cov=profiles --cov=oc_lettings_site --cov-report=html:cov_html + +Automatic tests execution +^^^^^^^^^^^^^^^^^^^^^^^^^ +The Orange County Lettings application uses a CI/CD pipeline (detailed in :doc:`deployment section `) that automatically runs tests during the continuous integration task. + +This pipeline uses a `setup.cfg` file containing the test command that generates a new report after each commit and push to your Git platform. + +Linting +------- +This Django project uses the Flake 8 linter that ensures you to implement an always-standardized code and according to the PEP 8 convention. + +Environment configuration +^^^^^^^^^^^^^^^^^^^^^^^^^ +In the `env` virtual environment, you must install Flake 8 library if not already installed as explained in the :doc:`installation section `. + +.. code:: + + pip install flake8 + +.. code:: + + poetry add flake8 + +.. code:: + + uv add flake8 + +Flake 8 report +^^^^^^^^^^^^^^ +To complete the flake 8 process by adding an HTML report, the `flake8-html` library is used to. + +To generate another report, you must install `flake8-html` before. +Please use the same procedure as above for `flake8`. + +Flake 8 local execution +^^^^^^^^^^^^^^^^^^^^^^^ +To generate another flake 8 linting process in your terminal, please type the line below : + +.. code:: + + flake8 --format=html --htmldir=flake8-report --max-line-length=119 --extend-exclude="env/, env-docs/" + +Automatic linting +^^^^^^^^^^^^^^^^^ +The Orange County Lettings application uses a CI/CD pipeline (detailed in :doc:`deployment section `) that automatically runs Flake 8 linter during the continuous integration task. + +This pipeline uses a `setup.cfg` file containing the Flake 8 command to generate a new report after each commit and push to your Git platform. diff --git a/docs/source/quick_start.rst b/docs/source/quick_start.rst new file mode 100644 index 0000000000..4890cf8918 --- /dev/null +++ b/docs/source/quick_start.rst @@ -0,0 +1,69 @@ +Quick start guide +================= + +Launching server +---------------- + +Local server +^^^^^^^^^^^^ +Please follow the steps as below : + +* Open a terminal +* Go to project folder - example : + + .. code:: + + cd oc_lettings_site + +* Activate the virtual environment as described previously +* Create environment variables (to avoid to add raw Sentry key into the code) : + + * With Power Shell : + + .. code:: + + $env:SENTRY_KEY = "your_key" + + * With Git Bash : + + .. code:: + + export SENTRY_KEY = "your_key" + +* Launch the local server by typing the command : + + .. code:: + + python manage.py runserver + +Web server +^^^^^^^^^^ + +Please follow the procedure described in :doc:`deployment section ` regarding the GitHub Actions or Gitlab CI/CD workflow, Docker containerization +and automatic deployment. + +Launching the APP +----------------- + +Please follow the steps as below : + +* With local server, open a web browser and type the urls : + + .. code:: + + http://127.0.0.1:8000/ + + .. code:: + + http://127.0.0.1:8000/admin + + for the admin panel (username: `admin`, password: given in the project technical specifications) + +* With web server (after deployment), open a web browser and type the url : + + * Your Heroku app url given in the Heroku dashboard, for example the url below : + + `Heroku app url example `_ + +You also need to specify all required environment variables used by the application in your Heroku app. +Please follow the procedure detailed in the Heroku sub-section in :doc:`deployment section `. diff --git a/docs/source/stack.rst b/docs/source/stack.rst new file mode 100644 index 0000000000..a6f92d5ec4 --- /dev/null +++ b/docs/source/stack.rst @@ -0,0 +1,11 @@ +Stack +===== +The stack used for developing Orange County Lettings new version is as below : + +* Python 3.10 and some libraries (:doc:`see installation section `) for app code +* Pytest library for testing +* SQLite database +* Docker for image containerization +* Heroku (and Gunicorn library) for web deployment +* Sentry for logs and exceptions monitoring +* ReadTheDocs for the application documentation diff --git a/docs/source/use_cases.rst b/docs/source/use_cases.rst new file mode 100644 index 0000000000..4aca5b3224 --- /dev/null +++ b/docs/source/use_cases.rst @@ -0,0 +1,68 @@ +User guide with use cases +========================= + +Quick overview +-------------- +Orange County Lettings allows the user to browse available rentals and all the registered profiles. + +The user has access to lettings and profiles lists, and can display a letting or a profile detail by id or username. + +Example of main use cases +------------------------- + +Home page +^^^^^^^^^ +.. image:: _static/home_page_screenshot.png + +Lettings list page +^^^^^^^^^^^^^^^^^^ +.. image:: _static/lettings_list_screenshot.png + +Letting details page +^^^^^^^^^^^^^^^^^^^^ +.. image:: _static/letting_details_screenshot.png + +Profiles list page +^^^^^^^^^^^^^^^^^^ +.. image:: _static/profiles_list_screenshot.png + +Profile details page +^^^^^^^^^^^^^^^^^^^^ +.. image:: _static/profile_details_screenshot.png + +Letting 404 error page +^^^^^^^^^^^^^^^^^^^^^^ +.. image:: _static/letting_404_error_screenshot.png + +Use cases +--------- + +Browsing lettings +^^^^^^^^^^^^^^^^^ +Here is the procedure : + +* Open the home page + +* Click on the "Letting" link + +* Select a letting from the list + +* The detail page displays the address and related information + +Viewing profiles +^^^^^^^^^^^^^^^^ +The procedure is more or less the same : + +* Open the Profiles page. + +* Select a profile from the list. + +* The application displays the user profile details. + +Returning to home page +^^^^^^^^^^^^^^^^^^^^^^ +Here is the procedure : + +* In the lettings page or profiles page, click on the "Home" button + +* The application displays the home page diff --git a/flake8-report/back.svg b/flake8-report/back.svg new file mode 100644 index 0000000000..ce80d2e6da --- /dev/null +++ b/flake8-report/back.svg @@ -0,0 +1,73 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/flake8-report/file.svg b/flake8-report/file.svg new file mode 100644 index 0000000000..98706cfe53 --- /dev/null +++ b/flake8-report/file.svg @@ -0,0 +1,64 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/flake8-report/index.html b/flake8-report/index.html new file mode 100644 index 0000000000..195631a111 --- /dev/null +++ b/flake8-report/index.html @@ -0,0 +1,30 @@ + + + + flake8 violations + + + + +
+
+

flake8 violations

+

Generated on 2026-05-22 21:12 + with mccabe: 0.6.1, pycodestyle: 2.5.0, pyflakes: 2.1.1 +

+
    + +
  • +
    + + + +

    All good!

    +

    No flake8 errors found in 32 files scanned.

    +
    +
  • + +
+
+ + \ No newline at end of file diff --git a/flake8-report/styles.css b/flake8-report/styles.css new file mode 100644 index 0000000000..6e0e447a6e --- /dev/null +++ b/flake8-report/styles.css @@ -0,0 +1,327 @@ +html { + font-family: sans-serif; + font-size: 90%; +} + +#masthead { + position: fixed; + left: 0; + top: 0; + right: 0; + height: 40%; +} + +h1, h2 { + font-family: sans-serif; + font-weight: normal; +} + +h1 { + color: white; + font-size: 36px; + margin-top: 1em; +} + +h1 img { + margin-right: 0.3em; +} + +h2 { + margin-top: 0; +} + +h1 a { + color: white; +} + +#versions { + color: rgba(255, 255, 255, 0.7); +} + +#page { + position: relative; + max-width: 960px; + margin: 0 auto; +} + +#index { + background-color: white; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.8); + padding: 0; + margin: 0; +} + +#index li { + list-style: none; + margin: 0; + padding: 1px 0; +} + +#index li + li { + border-top: solid silver 1px; +} + +.details p { + margin-left: 3em; + color: #888; +} + +#index a { + display: block; + padding: 0.8em 1em; + cursor: pointer; +} + +#index #all-good { + padding: 1.4em 1em 0.8em; +} + +#all-good .count .tick { + font-size: 2em; +} + +#all-good .count { + float: left; +} + +#all-good h2, +#all-good p { + margin-left: 50px; +} + +#index a:hover { + background-color: #eee; +} + +.count { + display: inline-block; + border-radius: 50%; + text-align: center; + width: 2.5em; + line-height: 2.5em; + height: 2.5em; + color: white; + margin-right: 1em; +} + +.sev-1 { + background-color: #a00; +} +.sev-2 { + background-color: #b80; +} +.sev-3 { + background-color: #28c; +} +.sev-4 { + background-color: #383; +} + +a { + text-decoration: none; +} + +#doc { + background-color: white; + margin: 1em 0; + padding: 1em; + padding-left: 1.2em; + position: relative; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.8); +} + +#doc pre { + margin: 0; + padding: 0.07em; +} + +.violations { + position: absolute; + margin: 1.2em 0 0 3em; + padding: 0.5em 1em; + font-size: 14px; + background-color: white; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.4); + display: none; +} + +.violations .count { + font-size: 70%; +} + +.violations li { + padding: 0.1em 0.3em; + list-style: none; +} + +.line-violations::before { + display: block; + content: ""; + position: absolute; + left: -1em; + width: 14px; + height: 14px; + border-radius: 50%; + background-color: red; +} + +.code:hover .violations { + display: block; +} + +tt { + white-space: pre-wrap; + font-family: Consolas, monospace; + font-size: 10pt; +} + +tt i { + color: silver; + display: inline-block; + text-align: right; + width: 3em; + box-sizing: border-box; + height: 100%; + border-right: solid #eee 1px; + padding-right: 0.2em; +} + +.le { + background-color: #ffe8e8; + cursor: pointer; +} + +.le:hover { + background-color: #fcc; +} + +.details { + clear: both; +} + +#index .details { + border-top-style: none; + margin: 1em; +} + +ul.details { + margin-left: 0; + padding-left: 0; +} + +#index .details li { + list-style: none; + border-top-style: none; + margin: 0.3em 0; + padding: 0; +} + +#srclink { + float: right; + font-size: 36px; + margin: 0; +} + +#srclink a { + color: white; +} + +#index .details a { + padding: 0; + color: inherit; +} + +.le { + background-color: #ffe8e8; + cursor: pointer; +} + +.le.sev-1 { + background-color: #f88; +} +.le.sev-2 { + background-color: #fda; +} +.le.sev-3 { + background-color: #adf; +} + +img { + height: 1.2em; + vertical-align: -0.35em; +} + +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.hll { background-color: #ffffcc } +.c { color: #3D7B7B; font-style: italic } /* Comment */ +.err { border: 1px solid #F00 } /* Error */ +.k { color: #008000; font-weight: bold } /* Keyword */ +.o { color: #666 } /* Operator */ +.ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ +.cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ +.cp { color: #9C6500 } /* Comment.Preproc */ +.cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ +.c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ +.cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ +.gd { color: #A00000 } /* Generic.Deleted */ +.ge { font-style: italic } /* Generic.Emph */ +.ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.gr { color: #E40000 } /* Generic.Error */ +.gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.gi { color: #008400 } /* Generic.Inserted */ +.go { color: #717171 } /* Generic.Output */ +.gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.gs { font-weight: bold } /* Generic.Strong */ +.gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.gt { color: #04D } /* Generic.Traceback */ +.kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.kp { color: #008000 } /* Keyword.Pseudo */ +.kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.kt { color: #B00040 } /* Keyword.Type */ +.m { color: #666 } /* Literal.Number */ +.s { color: #BA2121 } /* Literal.String */ +.na { color: #687822 } /* Name.Attribute */ +.nb { color: #008000 } /* Name.Builtin */ +.nc { color: #00F; font-weight: bold } /* Name.Class */ +.no { color: #800 } /* Name.Constant */ +.nd { color: #A2F } /* Name.Decorator */ +.ni { color: #717171; font-weight: bold } /* Name.Entity */ +.ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ +.nf { color: #00F } /* Name.Function */ +.nl { color: #767600 } /* Name.Label */ +.nn { color: #00F; font-weight: bold } /* Name.Namespace */ +.nt { color: #008000; font-weight: bold } /* Name.Tag */ +.nv { color: #19177C } /* Name.Variable */ +.ow { color: #A2F; font-weight: bold } /* Operator.Word */ +.w { color: #BBB } /* Text.Whitespace */ +.mb { color: #666 } /* Literal.Number.Bin */ +.mf { color: #666 } /* Literal.Number.Float */ +.mh { color: #666 } /* Literal.Number.Hex */ +.mi { color: #666 } /* Literal.Number.Integer */ +.mo { color: #666 } /* Literal.Number.Oct */ +.sa { color: #BA2121 } /* Literal.String.Affix */ +.sb { color: #BA2121 } /* Literal.String.Backtick */ +.sc { color: #BA2121 } /* Literal.String.Char */ +.dl { color: #BA2121 } /* Literal.String.Delimiter */ +.sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.s2 { color: #BA2121 } /* Literal.String.Double */ +.se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +.sh { color: #BA2121 } /* Literal.String.Heredoc */ +.si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +.sx { color: #008000 } /* Literal.String.Other */ +.sr { color: #A45A77 } /* Literal.String.Regex */ +.s1 { color: #BA2121 } /* Literal.String.Single */ +.ss { color: #19177C } /* Literal.String.Symbol */ +.bp { color: #008000 } /* Name.Builtin.Pseudo */ +.fm { color: #00F } /* Name.Function.Magic */ +.vc { color: #19177C } /* Name.Variable.Class */ +.vg { color: #19177C } /* Name.Variable.Global */ +.vi { color: #19177C } /* Name.Variable.Instance */ +.vm { color: #19177C } /* Name.Variable.Magic */ +.il { color: #666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/heroku.yml b/heroku.yml new file mode 100644 index 0000000000..8eec25b9c9 --- /dev/null +++ b/heroku.yml @@ -0,0 +1,3 @@ +build: + docker: + web: Dockerfile diff --git a/lettings/__init__.py b/lettings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lettings/admin.py b/lettings/admin.py new file mode 100644 index 0000000000..2b5abe26f8 --- /dev/null +++ b/lettings/admin.py @@ -0,0 +1,10 @@ +""" +Admin registration module for lettings app +""" +from django.contrib import admin + +from lettings.models import Address, Letting + + +admin.site.register(Letting) +admin.site.register(Address) diff --git a/lettings/apps.py b/lettings/apps.py new file mode 100644 index 0000000000..9a30d0d289 --- /dev/null +++ b/lettings/apps.py @@ -0,0 +1,13 @@ +""" +Namespace module for lettings app +""" +from django.apps import AppConfig + + +class LettingsConfig(AppConfig): + """ + Namespace class for Lettings + Attributes: + name: namespace of the lettings app + """ + name = 'lettings' diff --git a/lettings/migrations/0001_initial.py b/lettings/migrations/0001_initial.py new file mode 100644 index 0000000000..9a008d3d77 --- /dev/null +++ b/lettings/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 3.0 on 2026-05-18 12:53 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Address', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('number', models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(9999)])), + ('street', models.CharField(max_length=64)), + ('city', models.CharField(max_length=64)), + ('state', models.CharField(max_length=2, validators=[django.core.validators.MinLengthValidator(2)])), + ('zip_code', models.PositiveIntegerField(validators=[django.core.validators.MaxValueValidator(99999)])), + ('country_iso_code', models.CharField(max_length=3, validators=[django.core.validators.MinLengthValidator(3)])), + ], + ), + migrations.CreateModel( + name='Letting', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=256)), + ('address', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='new_letting', to='lettings.Address')), + ], + ), + ] diff --git a/lettings/migrations/0002_auto_20260518_1543.py b/lettings/migrations/0002_auto_20260518_1543.py new file mode 100644 index 0000000000..47cb6b8db2 --- /dev/null +++ b/lettings/migrations/0002_auto_20260518_1543.py @@ -0,0 +1,48 @@ +# Generated by Django 3.0 on 2026-05-18 13:43 + +from django.db import migrations + + +def copy_address(apps, schema_editor): + OldAddress = apps.get_model('oc_lettings_site', 'Address') + NewAddress = apps.get_model('lettings', 'Address') + + for obj in OldAddress.objects.all(): + NewAddress.objects.create( + id=obj.id, + number=obj.number, + street=obj.street, + city=obj.city, + state=obj.state, + zip_code=obj.zip_code, + country_iso_code=obj.country_iso_code, + ) + + +def copy_lettings(apps, schema_editor): + OldLetting = apps.get_model('oc_lettings_site', 'Letting') + NewLetting = apps.get_model('lettings', 'Letting') + NewAddress = apps.get_model('lettings', 'Address') + + for obj in OldLetting.objects.all(): + + new_address = NewAddress.objects.get(id=obj.address.id) + + NewLetting.objects.create( + id=obj.id, + title=obj.title, + address=new_address, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('lettings', '0001_initial'), + ('oc_lettings_site', '0001_initial'), + ] + + operations = [ + migrations.RunPython(copy_address), + migrations.RunPython(copy_lettings), + ] diff --git a/lettings/migrations/0003_auto_20260518_1551.py b/lettings/migrations/0003_auto_20260518_1551.py new file mode 100644 index 0000000000..11b098bd67 --- /dev/null +++ b/lettings/migrations/0003_auto_20260518_1551.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0 on 2026-05-18 13:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('lettings', '0002_auto_20260518_1543'), + ] + + operations = [ + migrations.AlterField( + model_name='letting', + name='address', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='lettings.Address'), + ), + ] diff --git a/lettings/migrations/__init__.py b/lettings/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lettings/models.py b/lettings/models.py new file mode 100644 index 0000000000..081de2a663 --- /dev/null +++ b/lettings/models.py @@ -0,0 +1,58 @@ +""" +Models module for lettings app +""" +from django.db import models +from django.core.validators import MaxValueValidator, MinLengthValidator + + +class Address(models.Model): + """ + Address model for Lettings + Attributes: + number (int): Letting number + street (str): Street address + city (str): City address + state (str): State address + zip_code (int): Zip code + country_iso_code (int): Country code + """ + number = models.PositiveIntegerField(validators=[MaxValueValidator(9999)]) + street = models.CharField(max_length=64) + city = models.CharField(max_length=64) + state = models.CharField(max_length=2, validators=[MinLengthValidator(2)]) + zip_code = models.PositiveIntegerField(validators=[MaxValueValidator(99999)]) + country_iso_code = models.CharField(max_length=3, validators=[MinLengthValidator(3)]) + + class Meta: + """ + Meta class for Lettings to specify verbose names + """ + verbose_name = "Address" + verbose_name_plural = "Addresses" + + def __str__(self) -> str: + """ + string method for Lettings + Returns: + A f-string with number and street address + """ + return f'{self.number} {self.street}' + + +class Letting(models.Model): + """ + Letting model for Lettings + Attributes: + title (str): Letting title + address (Address): Letting address + """ + title = models.CharField(max_length=256) + address = models.OneToOneField(Address, on_delete=models.CASCADE) + + def __str__(self) -> str: + """ + String method for Lettings + Returns: + A f-string with letting title + """ + return self.title diff --git a/templates/lettings_index.html b/lettings/templates/lettings/index.html similarity index 86% rename from templates/lettings_index.html rename to lettings/templates/lettings/index.html index 92857a78d9..ef866a010f 100644 --- a/templates/lettings_index.html +++ b/lettings/templates/lettings/index.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "oc_lettings_site/base.html" %} {% block title %}Lettings{% endblock title %} {% block content %} @@ -20,7 +20,7 @@

Lettings

@@ -36,7 +36,7 @@

Lettings

Home - + Profiles
diff --git a/templates/letting.html b/lettings/templates/lettings/letting.html similarity index 92% rename from templates/letting.html rename to lettings/templates/lettings/letting.html index 7e5f3a73fd..edb9870a01 100644 --- a/templates/letting.html +++ b/lettings/templates/lettings/letting.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "oc_lettings_site/base.html" %} {% load static %} {% block title %}{{ title }}{% endblock title %} @@ -25,14 +25,14 @@

{{ title }}

diff --git a/lettings/tests/__init__.py b/lettings/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lettings/tests/conftest.py b/lettings/tests/conftest.py new file mode 100644 index 0000000000..66ffe3eac4 --- /dev/null +++ b/lettings/tests/conftest.py @@ -0,0 +1,46 @@ +""" +Fixture module for lettings tests +""" +import pytest + +from _pytest.monkeypatch import MonkeyPatch + +from lettings.models import Address, Letting + + +@pytest.fixture(autouse=True) +def disable_sentry(monkeypatch: MonkeyPatch): + monkeypatch.setattr( + "monitoring.sentry_sdk.init", + lambda *args, **kwargs: None + ) + + +@pytest.fixture +def get_address(): + """ + Fixture that returns fictive address + Returns: + The address object + """ + return Address.objects.create( + number=500, + street="Address test", + city="City test", + state="State test", + zip_code=99999, + country_iso_code="Country iso code test" + ) + + +@pytest.fixture +def get_letting(get_address: Address): + """ + Fixture that returns fictive letting + Returns: + The letting object + """ + return Letting.objects.create( + title="Test Letting", + address=get_address + ) diff --git a/lettings/tests/tests_model.py b/lettings/tests/tests_model.py new file mode 100644 index 0000000000..74f035e3bc --- /dev/null +++ b/lettings/tests/tests_model.py @@ -0,0 +1,20 @@ +""" +Tests module for lettings app models +""" +import pytest + +from lettings.models import Letting, Address + + +class TestLettingsModel: + @pytest.mark.django_db + def test_lettings_address_model_ok(self, get_address: Address): + expected = f"{get_address.number} {get_address.street}" + + assert str(get_address) == expected + + @pytest.mark.django_db + def test_lettings_letting_model_ok(self, get_letting: Letting): + expected = f"{get_letting.title}" + + assert str(get_letting) == expected diff --git a/lettings/tests/tests_url.py b/lettings/tests/tests_url.py new file mode 100644 index 0000000000..0a7309ee62 --- /dev/null +++ b/lettings/tests/tests_url.py @@ -0,0 +1,23 @@ +""" +Tests module for lettings app urls +""" +import pytest + +from django.urls import reverse, resolve + +from lettings.models import Letting, Address + + +class TestLettingsUrl: + def test_lettings_index_url(self): + path = reverse(viewname="lettings:index") + + assert path == "/lettings/" + assert resolve(path).view_name == "lettings:index" + + @pytest.mark.django_db + def test_lettings_letting_url(self, get_address: Address, get_letting: Letting): + path = reverse(viewname="lettings:letting", kwargs={"letting_id": get_letting.id}) + + assert path == "/lettings/1/" + assert resolve(path).view_name == "lettings:letting" diff --git a/lettings/tests/tests_view.py b/lettings/tests/tests_view.py new file mode 100644 index 0000000000..e6c55b8ca5 --- /dev/null +++ b/lettings/tests/tests_view.py @@ -0,0 +1,102 @@ +""" +Tests module for lettings app views +""" +import pytest + +from django.urls import reverse +from django.test import Client +from pytest_django.asserts import assertTemplateUsed +from _pytest.monkeypatch import MonkeyPatch + +from lettings.models import Letting, Address + + +class TestLettingsView: + @pytest.mark.django_db + def test_lettings_index_view_ok(self, get_letting: Letting): + client = Client() + path = reverse(viewname="lettings:index") + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = '

Lettings

' + expected_content = f'{get_letting.title}' + + assert expected_h1 in content + assert expected_content in content + assert response.status_code == 200 + assertTemplateUsed(response, template_name="lettings/index.html") + + @pytest.mark.django_db + def test_lettings_index_view_returns_500(self, monkeypatch: MonkeyPatch, get_letting: Letting): + def raise_error(): + raise Exception("forced error") + + monkeypatch.setattr("lettings.views.Letting.objects.all", raise_error) + + client = Client() + path = reverse(viewname="lettings:index") + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = (f'

500 Error : ' + f'something wrong with the server - forced error

') + + assert expected_h1 in content + assert response.status_code == 500 + assertTemplateUsed(response, template_name="oc_lettings_site/error_500.html") + + @pytest.mark.django_db + def test_lettings_letting_view_ok(self, get_address: Address, get_letting: Letting): + client = Client() + path = reverse(viewname="lettings:letting", kwargs={"letting_id": get_letting.id}) + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = f'

{get_letting.title}

' + expected_content = [f'{get_address.number} {get_address.street}', + f'{get_address.city}, {get_address.state}', + f'{get_address.zip_code}', + f'{get_address.country_iso_code}'] + + assert expected_h1 in content + for expected_child in expected_content: + assert expected_child in content + assert response.status_code == 200 + assertTemplateUsed(response, template_name="lettings/letting.html") + + @pytest.mark.django_db + def test_lettings_letting_view_returns_404(self, get_address: Address, get_letting: Letting): + client = Client() + path = reverse(viewname="lettings:letting", kwargs={"letting_id": 2}) + + response = client.get(path=path) + content = response.content.decode() + + expected_h1 = (f'

404 Error : ' + f'letting n\xb0 2 not found !

') + + assert expected_h1 in content + assert response.status_code == 404 + assertTemplateUsed(response, template_name="oc_lettings_site/error_404.html") + + @pytest.mark.django_db + def test_lettings_letting_view_returns_500(self, + monkeypatch: MonkeyPatch, + get_letting: Letting): + def raise_error(*args, **kwargs): + raise Exception("forced error") + + monkeypatch.setattr("lettings.views.Letting.objects.get", raise_error) + + client = Client() + path = reverse(viewname="lettings:letting", kwargs={"letting_id": get_letting.id}) + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = (f'

500 Error : ' + f'something wrong with the server - forced error

') + + assert expected_h1 in content + assert response.status_code == 500 + assertTemplateUsed(response, template_name="oc_lettings_site/error_500.html") diff --git a/lettings/urls.py b/lettings/urls.py new file mode 100644 index 0000000000..61a7ca5c89 --- /dev/null +++ b/lettings/urls.py @@ -0,0 +1,13 @@ +""" +URLs module for lettings app +""" +from django.urls import path + +from . import views + +app_name = 'lettings' + +urlpatterns = [ + path('', views.index, name='index'), + path('/', views.letting, name='letting'), +] diff --git a/lettings/views.py b/lettings/views.py new file mode 100644 index 0000000000..b5be6997e4 --- /dev/null +++ b/lettings/views.py @@ -0,0 +1,101 @@ +""" +Views module for lettings app +""" +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render + +from .models import Letting +from monitoring import logger + + +# Aenean leo magna, vestibulum et tincidunt fermentum, consectetur quis velit. Sed non placerat +# massa. Integer est nunc, pulvinar a tempor et, bibendum id arcu. Vestibulum ante ipsum primis in +# faucibus orci luctus et ultrices posuere cubilia curae; Cras eget scelerisque +def index(request: HttpRequest) -> HttpResponse: + """ + View function for lettings index page + Args: + request (HttpRequest): Http Request object + + Returns: + An HTTP response with the list of lettings or an HTTP response with 500 error. + """ + try: + lettings_list = Letting.objects.all() + context = {'lettings_list': lettings_list} + + logger.info(f"Going to lettings index page : {context=}, status = 200.") + + return render(request=request, + template_name='lettings/index.html', + context=context, + status=200) + + except Exception as e: + context = {"error": str(e)} + + logger.error(f"Error 500 returned while reaching lettings index page : {context=}," + f" status = 500.") + + return render(request=request, + template_name='oc_lettings_site/error_500.html', + context=context, + status=500) + + +# Cras ultricies dignissim purus, vitae hendrerit ex varius non. In accumsan porta nisl id +# eleifend. Praesent dignissim, odio eu consequat pretium, purus urna vulputate arcu, vitae +# efficitur lacus justo nec purus. Aenean finibus faucibus lectus at porta. Maecenas auctor, est ut +# luctus congue, dui enim mattis enim, ac condimentum velit libero in magna. Suspendisse potenti. +# In tempus a nisi sed laoreet. Suspendisse porta dui eget sem accumsan interdum. Ut quis urna +# pellentesque justo mattis ullamcorper ac non tellus. In tristique mauris eu velit fermentum, +# tempus pharetra est luctus. Vivamus consequat aliquam libero, eget bibendum lorem. Sed non dolor +# risus. Mauris condimentum auctor elementum. Donec quis nisi ligula. Integer vehicula tincidunt +# enim, ac lacinia augue pulvinar sit amet. +def letting(request: HttpRequest, letting_id: int) -> HttpResponse: + """ + View function for letting detail page + Args: + request (HttpRequest): Http Request object + letting_id (int): letting id + + Returns: + An HTTP response with the letting detail or an HTTP response with 404 error if not found + or an HTTP response with 500 error + """ + try: + letting = Letting.objects.get(id=letting_id) + + context = { + 'title': letting.title, + 'address': letting.address, + } + + logger.info(f"Going to lettings details page : {context=}, status = 200.") + + return render(request=request, + template_name='lettings/letting.html', + context=context, + status=200) + + except Letting.DoesNotExist as e: + context = {"type": "letting", "id": letting_id, "error": str(e)} + + logger.warning(f"Error 404 returned while reaching letting n°{letting_id} : {context=}," + f" status = 404.") + + return render(request=request, + template_name='oc_lettings_site/error_404.html', + context=context, + status=404) + + except Exception as e: + context = {"error": str(e)} + + logger.error(f"Error 500 returned while reaching letting details page : {context=}," + f" status = 500.") + + return render(request=request, + template_name='oc_lettings_site/error_500.html', + context=context, + status=500) diff --git a/manage.py b/manage.py index c0e27e034a..961c945a68 100755 --- a/manage.py +++ b/manage.py @@ -1,3 +1,6 @@ +""" +Main module +""" import os import sys diff --git a/monitoring.py b/monitoring.py new file mode 100644 index 0000000000..309dd0ca46 --- /dev/null +++ b/monitoring.py @@ -0,0 +1,35 @@ +""" +Module for monitoring with Sentry +""" +import logging +import sentry_sdk + +from sentry_sdk.integrations.logging import LoggingIntegration + +from config import SENTRY_KEY + +logger = logging.getLogger(__name__) + + +def init_sentry(): + """ + Method to initialize sentry integration + """ + logging.basicConfig(level=logging.INFO) + + sentry_logging = LoggingIntegration( + level=logging.INFO, + event_level=logging.ERROR, + ) + + sentry_sdk.init( + dsn=SENTRY_KEY, + # Add request headers and IP for users, + # see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info + send_default_pii=True, + + # Enable logs to be sent to Sentry + enable_logs=True, + + integrations=[sentry_logging] + ) diff --git a/oc-lettings-site.sqlite3 b/oc-lettings-site.sqlite3 index 3d885414f9..92f150a14e 100644 Binary files a/oc-lettings-site.sqlite3 and b/oc-lettings-site.sqlite3 differ diff --git a/oc_lettings_site/admin.py b/oc_lettings_site/admin.py index 63328c6dd3..e69de29bb2 100644 --- a/oc_lettings_site/admin.py +++ b/oc_lettings_site/admin.py @@ -1,10 +0,0 @@ -from django.contrib import admin - -from .models import Letting -from .models import Address -from .models import Profile - - -admin.site.register(Letting) -admin.site.register(Address) -admin.site.register(Profile) diff --git a/oc_lettings_site/apps.py b/oc_lettings_site/apps.py index 6489692f04..805a860bb6 100644 --- a/oc_lettings_site/apps.py +++ b/oc_lettings_site/apps.py @@ -1,5 +1,13 @@ +""" +App config module for oc_lettings_site app +""" from django.apps import AppConfig class OCLettingsSiteConfig(AppConfig): + """ + Namespace class for oc_lettings_site app + Attributes: + name (str): Name of the app + """ name = 'oc_lettings_site' diff --git a/oc_lettings_site/migrations/0002_auto_20260518_1551.py b/oc_lettings_site/migrations/0002_auto_20260518_1551.py new file mode 100644 index 0000000000..586b4a1db8 --- /dev/null +++ b/oc_lettings_site/migrations/0002_auto_20260518_1551.py @@ -0,0 +1,32 @@ +# Generated by Django 3.0 on 2026-05-18 13:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('oc_lettings_site', '0001_initial'), + ('lettings', '0003_auto_20260518_1551'), + ('profiles', '0003_auto_20260518_1551'), + ] + + operations = [ + migrations.RemoveField( + model_name='letting', + name='address', + ), + migrations.RemoveField( + model_name='profile', + name='user', + ), + migrations.DeleteModel( + name='Address', + ), + migrations.DeleteModel( + name='Letting', + ), + migrations.DeleteModel( + name='Profile', + ), + ] diff --git a/oc_lettings_site/models.py b/oc_lettings_site/models.py deleted file mode 100644 index ed255e8c11..0000000000 --- a/oc_lettings_site/models.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.db import models -from django.core.validators import MaxValueValidator, MinLengthValidator -from django.contrib.auth.models import User - - -class Address(models.Model): - number = models.PositiveIntegerField(validators=[MaxValueValidator(9999)]) - street = models.CharField(max_length=64) - city = models.CharField(max_length=64) - state = models.CharField(max_length=2, validators=[MinLengthValidator(2)]) - zip_code = models.PositiveIntegerField(validators=[MaxValueValidator(99999)]) - country_iso_code = models.CharField(max_length=3, validators=[MinLengthValidator(3)]) - - def __str__(self): - return f'{self.number} {self.street}' - - -class Letting(models.Model): - title = models.CharField(max_length=256) - address = models.OneToOneField(Address, on_delete=models.CASCADE) - - def __str__(self): - return self.title - - -class Profile(models.Model): - user = models.OneToOneField(User, on_delete=models.CASCADE) - favorite_city = models.CharField(max_length=64, blank=True) - - def __str__(self): - return self.user.username diff --git a/oc_lettings_site/settings.py b/oc_lettings_site/settings.py index a18bee8106..3f3b8c5032 100644 --- a/oc_lettings_site/settings.py +++ b/oc_lettings_site/settings.py @@ -1,3 +1,6 @@ +""" +Settings module for oc_lettings_site app +""" import os from pathlib import Path @@ -10,18 +13,21 @@ # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'fp$9^593hsriajg$_%=5trot9g!1qa@ew(o-1#@=&4%=hp46(s' +SECRET_KEY = os.environ.get(key="SECRET_KEY", default="dummy-secret-key") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] +debug_var = os.environ.get(key="DEBUG", default="0") +DEBUG = True if debug_var and debug_var.lower() in ["true", "1", "yes"] else False +host_var = os.environ.get(key="DJANGO_ALLOWED_HOSTS", default="localhost,127.0.0.1") +ALLOWED_HOSTS = [e for e in host_var.split(",") if e] if host_var else [] # Application definition INSTALLED_APPS = [ 'oc_lettings_site.apps.OCLettingsSiteConfig', + 'lettings.apps.LettingsConfig', + 'profiles.apps.ProfilesConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -32,6 +38,7 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -45,7 +52,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'DIRS': [os.path.join(BASE_DIR, 'oc_lettings_site/templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -108,7 +115,14 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ +STATIC_URL = "/static/" + STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') -STATIC_URL = '/static/' -STATICFILES_DIRS = [BASE_DIR / "static",] +STATICFILES_DIRS = [BASE_DIR / "static", ] + +STORAGES = { + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} diff --git a/templates/base.html b/oc_lettings_site/templates/oc_lettings_site/base.html similarity index 97% rename from templates/base.html rename to oc_lettings_site/templates/oc_lettings_site/base.html index ab7addba01..403b342755 100644 --- a/templates/base.html +++ b/oc_lettings_site/templates/oc_lettings_site/base.html @@ -24,10 +24,10 @@
Logo Orange County Lettings diff --git a/oc_lettings_site/templates/oc_lettings_site/error_404.html b/oc_lettings_site/templates/oc_lettings_site/error_404.html new file mode 100644 index 0000000000..948099abf8 --- /dev/null +++ b/oc_lettings_site/templates/oc_lettings_site/error_404.html @@ -0,0 +1,30 @@ +{% extends "oc_lettings_site/base.html" %} +{% block title %}Holiday Homes{% endblock title %} + +{% block content %} + + +
+
+
+ {% if type == "letting" %} +

404 Error : {{ type }} n° {{ id }} not found !

+ {% elif type == "profile" %} +

404 Error : {{ type }} '{{ name }}' not found !

+ {% endif %} +
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/oc_lettings_site/templates/oc_lettings_site/error_500.html b/oc_lettings_site/templates/oc_lettings_site/error_500.html new file mode 100644 index 0000000000..a81c75049e --- /dev/null +++ b/oc_lettings_site/templates/oc_lettings_site/error_500.html @@ -0,0 +1,26 @@ +{% extends "oc_lettings_site/base.html" %} +{% block title %}Holiday Homes{% endblock title %} + +{% block content %} + + +
+
+
+

500 Error : something wrong with the server - {{ error }}

+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/oc_lettings_site/templates/oc_lettings_site/index.html similarity index 86% rename from templates/index.html rename to oc_lettings_site/templates/oc_lettings_site/index.html index 71a8e61a46..d668dd75ff 100644 --- a/templates/index.html +++ b/oc_lettings_site/templates/oc_lettings_site/index.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "oc_lettings_site/base.html" %} {% block title %}Holiday Homes{% endblock title %} {% block content %} @@ -14,10 +14,10 @@

Welcome to Holiday Homes

diff --git a/oc_lettings_site/tests.py b/oc_lettings_site/tests.py deleted file mode 100644 index 3fd62bb718..0000000000 --- a/oc_lettings_site/tests.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_dummy(): - assert 1 diff --git a/oc_lettings_site/tests/__init__.py b/oc_lettings_site/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/oc_lettings_site/tests/conftest.py b/oc_lettings_site/tests/conftest.py new file mode 100644 index 0000000000..943f1fbb0b --- /dev/null +++ b/oc_lettings_site/tests/conftest.py @@ -0,0 +1,14 @@ +""" +Fixture module for oc_lettings_site tests +""" +import pytest + +from _pytest.monkeypatch import MonkeyPatch + + +@pytest.fixture(autouse=True) +def disable_sentry(monkeypatch: MonkeyPatch): + monkeypatch.setattr( + "monitoring.sentry_sdk.init", + lambda *args, **kwargs: None + ) diff --git a/oc_lettings_site/tests/tests_url.py b/oc_lettings_site/tests/tests_url.py new file mode 100644 index 0000000000..1140fabfdd --- /dev/null +++ b/oc_lettings_site/tests/tests_url.py @@ -0,0 +1,12 @@ +""" +Tests module for oc_lettings_site app urls +""" +from django.urls import reverse, resolve + + +class TestOcLettingsSiteUrl: + def test_index_url(self): + path = reverse("index") + + assert path == "/" + assert resolve(path).view_name == 'index' diff --git a/oc_lettings_site/tests/tests_view.py b/oc_lettings_site/tests/tests_view.py new file mode 100644 index 0000000000..7fc564d074 --- /dev/null +++ b/oc_lettings_site/tests/tests_view.py @@ -0,0 +1,46 @@ +""" +Tests module for oc_lettings_site app views +""" +import pytest + +from django.template.response import TemplateResponse +from django.test import Client +from django.urls import reverse +from pytest_django.asserts import assertTemplateUsed +from _pytest.monkeypatch import MonkeyPatch + + +class TestOcLettingsSiteView: + @pytest.mark.django_db + def test_oc_lettings_site_index_view_ok(self): + client = Client() + path = reverse(viewname="index") + + response = client.get(path=path) + content = response.content.decode() + expected = "Welcome to Holiday Homes" + + assert expected in content + assert response.status_code == 200 + assertTemplateUsed(response, template_name="oc_lettings_site/index.html") + + @pytest.mark.django_db + def test_oc_lettings_site_index_view_returns_500(self, monkeypatch: MonkeyPatch): + def side_effect(request, template_name, context=None, status=500): + if template_name == "oc_lettings_site/index.html": + raise Exception("forced error") + return TemplateResponse(request, template_name, context or {}, status=status) + + monkeypatch.setattr("oc_lettings_site.views.render", side_effect) + + client = Client() + path = reverse(viewname="index") + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = (f'

500 Error : ' + f'something wrong with the server - forced error

') + + assert expected_h1 in content + assert response.status_code == 500 + assertTemplateUsed(response, template_name="oc_lettings_site/error_500.html") diff --git a/oc_lettings_site/urls.py b/oc_lettings_site/urls.py index f0ff5897ab..87eebf52d6 100644 --- a/oc_lettings_site/urls.py +++ b/oc_lettings_site/urls.py @@ -1,13 +1,15 @@ +""" +URLs module for oc_lettings_site app. +Include the lettings and profiles apps urls +""" from django.contrib import admin -from django.urls import path +from django.urls import path, include from . import views urlpatterns = [ path('', views.index, name='index'), - path('lettings/', views.lettings_index, name='lettings_index'), - path('lettings//', views.letting, name='letting'), - path('profiles/', views.profiles_index, name='profiles_index'), - path('profiles//', views.profile, name='profile'), + path('lettings/', include('lettings.urls')), + path('profiles/', include('profiles.urls')), path('admin/', admin.site.urls), ] diff --git a/oc_lettings_site/views.py b/oc_lettings_site/views.py index a72db27074..09ab96880e 100644 --- a/oc_lettings_site/views.py +++ b/oc_lettings_site/views.py @@ -1,45 +1,39 @@ +""" +Views module for oc_lettings_site app +""" +from django.http import HttpRequest, HttpResponse from django.shortcuts import render -from .models import Letting, Profile - - - - -# Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque molestie quam lobortis leo consectetur ullamcorper non id est. Praesent dictum, nulla eget feugiat sagittis, sem mi convallis eros, -# vitae dapibus nisi lorem dapibus sem. Maecenas pharetra purus ipsum, eget consequat ipsum lobortis quis. Phasellus eleifend ex auctor venenatis tempus. -# Aliquam vitae erat ac orci placerat luctus. Nullam elementum urna nisi, pellentesque iaculis enim cursus in. Praesent volutpat porttitor magna, non finibus neque cursus id. -def index(request): - return render(request, 'index.html') - -# Aenean leo magna, vestibulum et tincidunt fermentum, consectetur quis velit. Sed non placerat massa. Integer est nunc, pulvinar a -# tempor et, bibendum id arcu. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Cras eget scelerisque -def lettings_index(request): - lettings_list = Letting.objects.all() - context = {'lettings_list': lettings_list} - return render(request, 'lettings_index.html', context) - - -#Cras ultricies dignissim purus, vitae hendrerit ex varius non. In accumsan porta nisl id eleifend. Praesent dignissim, odio eu consequat pretium, purus urna vulputate arcu, vitae efficitur -# lacus justo nec purus. Aenean finibus faucibus lectus at porta. Maecenas auctor, est ut luctus congue, dui enim mattis enim, ac condimentum velit libero in magna. Suspendisse potenti. In tempus a nisi sed laoreet. -# Suspendisse porta dui eget sem accumsan interdum. Ut quis urna pellentesque justo mattis ullamcorper ac non tellus. In tristique mauris eu velit fermentum, tempus pharetra est luctus. Vivamus consequat aliquam libero, eget bibendum lorem. Sed non dolor risus. Mauris condimentum auctor elementum. Donec quis nisi ligula. Integer vehicula tincidunt enim, ac lacinia augue pulvinar sit amet. -def letting(request, letting_id): - letting = Letting.objects.get(id=letting_id) - context = { - 'title': letting.title, - 'address': letting.address, - } - return render(request, 'letting.html', context) - -# Sed placerat quam in pulvinar commodo. Nullam laoreet consectetur ex, sed consequat libero pulvinar eget. Fusc -# faucibus, urna quis auctor pharetra, massa dolor cursus neque, quis dictum lacus d -def profiles_index(request): - profiles_list = Profile.objects.all() - context = {'profiles_list': profiles_list} - return render(request, 'profiles_index.html', context) - -# Aliquam sed metus eget nisi tincidunt ornare accumsan eget lac -# laoreet neque quis, pellentesque dui. Nullam facilisis pharetra vulputate. Sed tincidunt, dolor id facilisis fringilla, eros leo tristique lacus, -# it. Nam aliquam dignissim congue. Pellentesque habitant morbi tristique senectus et netus et males -def profile(request, username): - profile = Profile.objects.get(user__username=username) - context = {'profile': profile} - return render(request, 'profile.html', context) + +from monitoring import init_sentry, logger + + +# Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque molestie quam lobortis leo +# consectetur ullamcorper non id est. Praesent dictum, nulla eget feugiat sagittis, sem mi +# convallis eros, vitae dapibus nisi lorem dapibus sem. Maecenas pharetra purus ipsum, eget +# consequat ipsum lobortis quis. Phasellus eleifend ex auctor venenatis tempus. Aliquam vitae erat +# ac orci placerat luctus. Nullam elementum urna nisi, pellentesque iaculis enim cursus in. +# Praesent volutpat porttitor magna, non finibus neque cursus id. +def index(request: HttpRequest) -> HttpResponse: + """ + View function for home page + Args: + request (HttpRequest): Http Request object + + Returns: + An HTTP response with index page or HTTP response with 500 error. + """ + init_sentry() + try: + logger.info(f"Going to home page : status = 200.") + + return render(request=request, template_name='oc_lettings_site/index.html') + + except Exception as e: + context = {'error': str(e)} + + logger.error(f"Error 500 returned while reaching home page : {context=}" + f", status = 500.") + + return render(request=request, + template_name='oc_lettings_site/error_500.html', + context=context) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000000..9b89467019 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,771 @@ +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. + +[[package]] +name = "asgiref" +version = "3.11.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133"}, + {file = "asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce"}, +] + +[package.dependencies] +typing_extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[package.extras] +tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] + +[[package]] +name = "certifi" +version = "2026.4.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"}, + {file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.14.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075"}, + {file = "coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec"}, + {file = "coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218"}, + {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85"}, + {file = "coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323"}, + {file = "coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a"}, + {file = "coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480"}, + {file = "coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0"}, + {file = "coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20"}, + {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c"}, + {file = "coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3"}, + {file = "coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1"}, + {file = "coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627"}, + {file = "coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5"}, + {file = "coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb"}, + {file = "coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5"}, + {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595"}, + {file = "coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27"}, + {file = "coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2"}, + {file = "coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d"}, + {file = "coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef"}, + {file = "coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2"}, + {file = "coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52"}, + {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe"}, + {file = "coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae"}, + {file = "coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e"}, + {file = "coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96"}, + {file = "coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90"}, + {file = "coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899"}, + {file = "coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47"}, + {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477"}, + {file = "coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab"}, + {file = "coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917"}, + {file = "coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8"}, + {file = "coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d"}, + {file = "coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8"}, + {file = "coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c"}, + {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca"}, + {file = "coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828"}, + {file = "coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d"}, + {file = "coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9"}, + {file = "coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1"}, + {file = "coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f"}, + {file = "coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6"}, + {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db"}, + {file = "coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2"}, + {file = "coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644"}, + {file = "coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b"}, + {file = "coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1"}, + {file = "coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "django" +version = "3.0" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "Django-3.0-py3-none-any.whl", hash = "sha256:6f857bd4e574442ba35a7172f1397b303167dae964cf18e53db5e85fe248d000"}, + {file = "Django-3.0.tar.gz", hash = "sha256:d98c9b6e5eed147bc51f47c014ff6826bd1ab50b166956776ee13db5a58804ae"}, +] + +[package.dependencies] +asgiref = ">=3.2,<4.0" +pytz = "*" +sqlparse = ">=0.2.2" + +[package.extras] +argon2 = ["argon2-cffi (>=16.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "entrypoints" +version = "0.3" +description = "Discover and load entry points from installed packages." +optional = false +python-versions = ">=2.7" +groups = ["main"] +files = [ + {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, + {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "flake8" +version = "3.7.0" +description = "the modular source code checker: pep8, pyflakes and co" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "flake8-3.7.0-py2.py3-none-any.whl", hash = "sha256:7eda8e5c29ac9e0d3c0fd44649298c29768efc79a9b872b41c8f05ca5f502c83"}, + {file = "flake8-3.7.0.tar.gz", hash = "sha256:2baac1c277d917f3e01ce17a60dcfad4a4ce13a2c5d50a15d0811302a3bdf7aa"}, +] + +[package.dependencies] +entrypoints = ">=0.3.0,<0.4.0" +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.5.0,<2.6.0" +pyflakes = ">=2.1.0,<2.2.0" + +[[package]] +name = "flake8-html" +version = "0.4.3" +description = "Generate HTML reports of flake8 violations" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "flake8-html-0.4.3.tar.gz", hash = "sha256:8b870299620cc4a06f73644a1b4d457799abeca1cc914c62ae71ec5bf65c79a5"}, + {file = "flake8_html-0.4.3-py2.py3-none-any.whl", hash = "sha256:8f126748b1b0edd6cd39e87c6192df56e2f8655b0aa2bb00ffeac8cf27be4325"}, +] + +[package.dependencies] +flake8 = ">=3.3.0" +jinja2 = ">=3.1.0" +pygments = ">=2.2.0" + +[[package]] +name = "gunicorn" +version = "26.0.0" +description = "WSGI HTTP Server for UNIX" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc"}, + {file = "gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +fast = ["gunicorn_h1c (>=0.6.5)"] +gevent = ["gevent (>=24.10.1)"] +http2 = ["h2 (>=4.1.0)"] +setproctitle = ["setproctitle"] +testing = ["coverage", "gevent (>=24.10.1)", "h2 (>=4.1.0)", "httpx[http2]", "pytest", "pytest-asyncio", "pytest-cov", "uvloop (>=0.19.0)"] +tornado = ["tornado (>=6.5.0)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] + +[[package]] +name = "packaging" +version = "26.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, + {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pycodestyle" +version = "2.5.0" +description = "Python style guide checker" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "pycodestyle-2.5.0-py2.py3-none-any.whl", hash = "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56"}, + {file = "pycodestyle-2.5.0.tar.gz", hash = "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"}, +] + +[[package]] +name = "pyflakes" +version = "2.1.1" +description = "passive checker of Python programs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "pyflakes-2.1.1-py2.py3-none-any.whl", hash = "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0"}, + {file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"}, +] + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "9.0.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, + {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"}, + {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"}, +] + +[package.dependencies] +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" + +[package.extras] +testing = ["process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-django" +version = "4.12.0" +description = "A Django plugin for pytest." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85"}, + {file = "pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[[package]] +name = "python-dotenv" +version = "1.2.2" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, + {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pytz" +version = "2026.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126"}, + {file = "pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a"}, +] + +[[package]] +name = "sentry-sdk" +version = "2.60.0" +description = "Python client for Sentry (https://sentry.io)" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "sentry_sdk-2.60.0-py3-none-any.whl", hash = "sha256:28a536c03291c8bcb363cf35c611b32738ec118ff64d8d6383b096448ac4c803"}, + {file = "sentry_sdk-2.60.0.tar.gz", hash = "sha256:0bd25e54e78ca02d0be512529fa644bbbf9e8470d7b26371294012d4ca93c978"}, +] + +[package.dependencies] +certifi = "*" +urllib3 = ">=1.26.11" + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +anthropic = ["anthropic (>=0.16)"] +arq = ["arq (>=0.23)"] +asyncio = ["httpcore[asyncio] (==1.*)"] +asyncpg = ["asyncpg (>=0.23)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +celery-redbeat = ["celery-redbeat (>=2)"] +chalice = ["chalice (>=1.16.0)"] +clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] +flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] +google-genai = ["google-genai (>=1.29.0)"] +grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"] +http2 = ["httpcore[http2] (==1.*)"] +httpx = ["httpx (>=0.16.0)"] +huey = ["huey (>=2)"] +huggingface-hub = ["huggingface_hub (>=0.22)"] +langchain = ["langchain (>=0.0.210)"] +langgraph = ["langgraph (>=0.6.6)"] +launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"] +litellm = ["litellm (>=1.77.5,!=1.82.7,!=1.82.8)"] +litestar = ["litestar (>=2.0.0)"] +loguru = ["loguru (>=0.5)"] +mcp = ["mcp (>=1.15.0)"] +openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] +openfeature = ["openfeature-sdk (>=0.7.1)"] +opentelemetry = ["opentelemetry-distro (>=0.35b0)"] +opentelemetry-experimental = ["opentelemetry-distro"] +opentelemetry-otlp = ["opentelemetry-distro[otlp] (>=0.35b0)"] +pure-eval = ["asttokens", "executing", "pure_eval"] +pydantic-ai = ["pydantic-ai (>=1.0.0)"] +pymongo = ["pymongo (>=3.1)"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] +starlite = ["starlite (>=1.48)"] +statsig = ["statsig (>=0.55.3)"] +tornado = ["tornado (>=6)"] +unleash = ["UnleashClient (>=6.0.1)"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sqlparse" +version = "0.5.5" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba"}, + {file = "sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e"}, +] + +[package.extras] +dev = ["build"] +doc = ["sphinx"] + +[[package]] +name = "tomli" +version = "2.4.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_full_version <= \"3.11.0a6\"" +files = [ + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version == \"3.10\"" +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "whitenoise" +version = "6.12.0" +description = "Radically simplified static file serving for WSGI applications" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2"}, + {file = "whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad"}, +] + +[package.extras] +brotli = ["brotli"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.10" +content-hash = "709d6c8d11abea173e14d844996776eddc689b7fdd8169e4f8ca42f6620db965" diff --git a/profiles/__init__.py b/profiles/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/profiles/admin.py b/profiles/admin.py new file mode 100644 index 0000000000..2b417bb0da --- /dev/null +++ b/profiles/admin.py @@ -0,0 +1,9 @@ +""" +Admin registration module for profiles app +""" +from django.contrib import admin + +from profiles.models import Profile + + +admin.site.register(Profile) diff --git a/profiles/apps.py b/profiles/apps.py new file mode 100644 index 0000000000..b0c6010f11 --- /dev/null +++ b/profiles/apps.py @@ -0,0 +1,13 @@ +""" +Namespace module for profiles app +""" +from django.apps import AppConfig + + +class ProfilesConfig(AppConfig): + """ + Namespace class for profiles app + Attributes: + name: namespace of the profiles app + """ + name = 'profiles' diff --git a/profiles/migrations/0001_initial.py b/profiles/migrations/0001_initial.py new file mode 100644 index 0000000000..069a3babcc --- /dev/null +++ b/profiles/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0 on 2026-05-18 12:53 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('favorite_city', models.CharField(blank=True, max_length=64)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='new_profile', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/profiles/migrations/0002_auto_20260518_1548.py b/profiles/migrations/0002_auto_20260518_1548.py new file mode 100644 index 0000000000..7be66c52bf --- /dev/null +++ b/profiles/migrations/0002_auto_20260518_1548.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0 on 2026-05-18 13:48 + +from django.db import migrations + + +def copy_profile(apps, schema_editor): + OldProfile = apps.get_model('oc_lettings_site', 'Profile') + NewProfile = apps.get_model('profiles', 'Profile') + + for obj in OldProfile.objects.all(): + NewProfile.objects.create( + id=obj.id, + user=obj.user, + favorite_city=obj.favorite_city, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0001_initial'), + ('oc_lettings_site', '0001_initial'), + ] + + operations = [ + migrations.RunPython(copy_profile), + ] diff --git a/profiles/migrations/0003_auto_20260518_1551.py b/profiles/migrations/0003_auto_20260518_1551.py new file mode 100644 index 0000000000..61fbe40024 --- /dev/null +++ b/profiles/migrations/0003_auto_20260518_1551.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0 on 2026-05-18 13:51 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('profiles', '0002_auto_20260518_1548'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='user', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/profiles/migrations/__init__.py b/profiles/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/profiles/models.py b/profiles/models.py new file mode 100644 index 0000000000..5893b3c972 --- /dev/null +++ b/profiles/models.py @@ -0,0 +1,24 @@ +""" +Models module for profiles app +""" +from django.db import models +from django.contrib.auth.models import User + + +class Profile(models.Model): + """ + Models class for profiles app + Attributes: + user (User): user + favorite_city (str): The favorite city of the user + """ + user = models.OneToOneField(User, on_delete=models.CASCADE) + favorite_city = models.CharField(max_length=64, blank=True) + + def __str__(self) -> str: + """ + String method for profile model + Returns: + The user name of the profile + """ + return self.user.username diff --git a/templates/profiles_index.html b/profiles/templates/profiles/index.html similarity index 85% rename from templates/profiles_index.html rename to profiles/templates/profiles/index.html index 4ad1daf92f..ed902a97d9 100644 --- a/templates/profiles_index.html +++ b/profiles/templates/profiles/index.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "oc_lettings_site/base.html" %} {% block title %}Profiles{% endblock title %} {% block content %} @@ -18,7 +18,7 @@

Profiles

@@ -34,7 +34,7 @@

Profiles

Home - + Lettings
diff --git a/templates/profile.html b/profiles/templates/profiles/profile.html similarity index 93% rename from templates/profile.html rename to profiles/templates/profiles/profile.html index d150d30e63..1f68a03a67 100644 --- a/templates/profile.html +++ b/profiles/templates/profiles/profile.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "oc_lettings_site/base.html" %} {% block title %}{{ profile.user.username }}{% endblock title %} {% block content %} @@ -24,14 +24,14 @@

{{ profile.user.username }}

diff --git a/profiles/tests/__init__.py b/profiles/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/profiles/tests/conftest.py b/profiles/tests/conftest.py new file mode 100644 index 0000000000..f6addc0755 --- /dev/null +++ b/profiles/tests/conftest.py @@ -0,0 +1,34 @@ +""" +Fixture module for profiles tests +""" +import pytest + +from _pytest.monkeypatch import MonkeyPatch +from django.contrib.auth.models import User + +from profiles.models import Profile + + +@pytest.fixture(autouse=True) +def disable_sentry(monkeypatch: MonkeyPatch): + monkeypatch.setattr( + "monitoring.sentry_sdk.init", + lambda *args, **kwargs: None + ) + + +@pytest.fixture +def get_profile(): + """ + Fixture that returns fictive profile + Returns: + The profile object + """ + return Profile.objects.create( + user=User.objects.create_user(username='Username', + first_name='First', + last_name='Last', + email='test@test.com', + password='test_pwd'), + favorite_city="City Test" + ) diff --git a/profiles/tests/tests_model.py b/profiles/tests/tests_model.py new file mode 100644 index 0000000000..44970b1485 --- /dev/null +++ b/profiles/tests/tests_model.py @@ -0,0 +1,14 @@ +""" +Tests module for profiles app models +""" +import pytest + +from profiles.models import Profile + + +class TestProfilesModel: + @pytest.mark.django_db + def test_profiles_profile_model_ok(self, get_profile: Profile): + expected = f"{get_profile.user.username}" + + assert str(get_profile) == expected diff --git a/profiles/tests/tests_url.py b/profiles/tests/tests_url.py new file mode 100644 index 0000000000..1d52508917 --- /dev/null +++ b/profiles/tests/tests_url.py @@ -0,0 +1,30 @@ +""" +Tests module for profiles app urls +""" +import pytest + +from django.contrib.auth.models import User +from django.urls import reverse, resolve + +from profiles.models import Profile + + +class TestProfilesUrl: + def test_profiles_index_url(self): + path = reverse("profiles:index") + + assert path == "/profiles/" + assert resolve(path).view_name == 'profiles:index' + + @pytest.mark.django_db + def test_profiles_profile_url(self): + Profile.objects.create(user=User.objects.create_user(username="Username", + first_name='First Name', + last_name='Last Name', + email='test@test.com'), + favorite_city="Paris") + + path = reverse(viewname="profiles:profile", kwargs={'username': "Username"}) + + assert path == "/profiles/Username/" + assert resolve(path).view_name == "profiles:profile" diff --git a/profiles/tests/tests_view.py b/profiles/tests/tests_view.py new file mode 100644 index 0000000000..eb6985242e --- /dev/null +++ b/profiles/tests/tests_view.py @@ -0,0 +1,104 @@ +""" +Tests module for profiles app views +""" +import pytest + +from django.urls import reverse +from django.test import Client +from pytest_django.asserts import assertTemplateUsed +from _pytest.monkeypatch import MonkeyPatch + +from profiles.models import Profile + + +class TestProfilesView: + @pytest.mark.django_db + def test_profiles_index_view_ok(self, get_profile: Profile): + client = Client() + path = reverse(viewname="profiles:index") + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = '

Profiles

' + expected_content = (f'' + f'{get_profile.user.username}') + + assert expected_h1 in content + assert expected_content in content + assert response.status_code == 200 + assertTemplateUsed(response, template_name="profiles/index.html") + + @pytest.mark.django_db + def test_profiles_index_view_returns_500(self, monkeypatch: MonkeyPatch, get_profile: Profile): + def raise_error(): + raise Exception("forced error") + + monkeypatch.setattr("profiles.views.Profile.objects.all", raise_error) + + client = Client() + path = reverse(viewname="profiles:index") + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = (f'

500 Error : ' + f'something wrong with the server - forced error

') + + assert expected_h1 in content + assert response.status_code == 500 + assertTemplateUsed(response, template_name="oc_lettings_site/error_500.html") + + @pytest.mark.django_db + def test_profiles_profile_view_ok(self, get_profile: Profile): + client = Client() + path = reverse(viewname="profiles:profile", kwargs={"username": get_profile.user.username}) + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = (f'

' + f'{get_profile.user.username}

') + expected_content = [f'First name : {get_profile.user.first_name}', + f'Last name : {get_profile.user.last_name}', + f'Email : {get_profile.user.email}', + f'Favorite city : {get_profile.favorite_city}'] + + assert expected_h1 in content + for expected_child in expected_content: + assert expected_child in content + assert response.status_code == 200 + assertTemplateUsed(response, template_name="profiles/profile.html") + + @pytest.mark.django_db + def test_profiles_profile_view_returns_404(self, get_profile: Profile): + client = Client() + path = reverse(viewname="profiles:profile", kwargs={"username": "test"}) + + response = client.get(path=path) + content = response.content.decode() + + expected_h1 = (f'

404 Error : ' + f'profile \'test\' not found !

') + + assert expected_h1 in content + assert response.status_code == 404 + assertTemplateUsed(response, template_name="oc_lettings_site/error_404.html") + + @pytest.mark.django_db + def test_profiles_profile_view_returns_500(self, + monkeypatch: MonkeyPatch, + get_profile: Profile): + def raise_error(*args, **kwargs): + raise Exception("forced error") + + monkeypatch.setattr("profiles.views.Profile.objects.get", raise_error) + + client = Client() + path = reverse(viewname="profiles:profile", kwargs={"username": get_profile.user.username}) + + response = client.get(path=path) + content = response.content.decode() + expected_h1 = (f'

500 Error : ' + f'something wrong with the server - forced error

') + + assert expected_h1 in content + assert response.status_code == 500 + assertTemplateUsed(response, template_name="oc_lettings_site/error_500.html") diff --git a/profiles/urls.py b/profiles/urls.py new file mode 100644 index 0000000000..1efb5f1a21 --- /dev/null +++ b/profiles/urls.py @@ -0,0 +1,13 @@ +""" +URLs module for profiles app +""" +from django.urls import path + +from . import views + +app_name = 'profiles' + +urlpatterns = [ + path('', views.index, name='index'), + path('/', views.profile, name='profile'), +] diff --git a/profiles/views.py b/profiles/views.py new file mode 100644 index 0000000000..649a97cf97 --- /dev/null +++ b/profiles/views.py @@ -0,0 +1,89 @@ +""" +Views module for profiles app +""" +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render +from monitoring import logger + +from profiles.models import Profile + + +# Sed placerat quam in pulvinar commodo. Nullam laoreet consectetur ex, sed consequat libero +# pulvinar eget. Fusc faucibus, urna quis auctor pharetra, massa dolor cursus neque, quis dictum +# lacus d +def index(request: HttpRequest) -> HttpResponse: + """ + View function for profiles index page + Args: + request (HttpRequest): request object + + Returns: + An HTTP response with the list of profiles or HTTP response with 500 error. + """ + try: + profiles_list = Profile.objects.all() + context = {'profiles_list': profiles_list} + + logger.info(f"Going to profiles index page : {context=}, status = 200.") + + return render(request=request, + template_name='profiles/index.html', + context=context, + status=200) + + except Exception as e: + context = {"error": str(e)} + + logger.error(f"Error 500 returned while reaching profiles index page : {context=}" + f", status = 500.") + + return render(request=request, + template_name='oc_lettings_site/error_500.html', + context=context, + status=500) + + +# Aliquam sed metus eget nisi tincidunt ornare accumsan eget lac +# laoreet neque quis, pellentesque dui. Nullam facilisis pharetra vulputate. Sed tincidunt, dolor +# id facilisis fringilla, eros leo tristique lacus, it. Nam aliquam dignissim congue. Pellentesque +# habitant morbi tristique senectus et netus et males +def profile(request: HttpRequest, username: str): + """ + View function for profile details page + Args: + request (HttpRequest): request object + username (str): username + + Returns: + An HTTP response with the profile or HTTP response with 404 error if not found + or an HTTP response with 500 error + """ + try: + profile = Profile.objects.get(user__username=username) + context = {'profile': profile} + + logger.info(f"Going to profile details page : {context=}, status = 200.") + + return render(request, template_name='profiles/profile.html', context=context, status=200) + + except Profile.DoesNotExist as e: + context = {"type": "profile", "name": username, "error": str(e)} + + logger.warning(f"Error 404 returned while reaching profile {username} : {context=}," + f" status = 404.") + + return render(request=request, + template_name='oc_lettings_site/error_404.html', + context=context, + status=404) + + except Exception as e: + context = {"error": str(e)} + + logger.error(f"Error 500 returned while reaching profile details page : {context=}," + f" status = 500.") + + return render(request=request, + template_name='oc_lettings_site/error_500.html', + context=context, + status=500) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..a13e3e2341 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "openclassrooms-project-13" +version = "0.1.0" +description = "" +authors = [ + {name = "NM",email = "nicolas.marie.nm@gmail.com"} +] +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "django (==3.0)", + "flake8 (==3.7.0)", + "flake8-html (==0.4.3)", + "pytest (==9.0.3)", + "pytest-django (==4.12.0)", + "pytest-cov (==7.1.0)", + "six (==1.17.0)", + "sentry-sdk (>=2.60.0,<3.0.0)", + "python-dotenv (>=1.2.2,<2.0.0)", + "gunicorn (>=26.0.0,<27.0.0)", + "whitenoise (>=6.12.0,<7.0.0)", +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000000..7e8c3144c3 Binary files /dev/null and b/requirements-docs.txt differ diff --git a/requirements.txt b/requirements.txt index c48c84ea40..6298a5a061 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/setup.cfg b/setup.cfg index 9346841bbc..6199fd8563 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,10 @@ [flake8] +format = html +htmldir = flake8-report max-line-length = 99 -exclude = **/migrations/*,venv +exclude = **/migrations/*,env,cov_html [tool:pytest] DJANGO_SETTINGS_MODULE = oc_lettings_site.settings -python_files = tests.py -addopts = -v +python_files = tests*.py +addopts = -v --cov=lettings --cov=profiles --cov=oc_lettings_site --cov-report=html:cov_html