From 34e9060737a453aa3fe43aff0837e842ac38f9f8 Mon Sep 17 00:00:00 2001 From: bakebot Date: Mon, 29 Jan 2024 09:25:22 +0000 Subject: [PATCH 1/9] Cookie updated by NetworkToCode Cookie Drift Manager Tool Template: ``` { "template": "https://github.com/nautobot/cookiecutter-nautobot-app.git", "dir": "nautobot-app", "ref": "refs/tags/nautobot-app-v2.1.0", "path": null } ``` Cookie: ``` { "remote": "https://github.com/nautobot/nautobot-app-floor-plan.git", "path": "/tmp/tmpfr39cfkg/nautobot-app-floor-plan", "repository_path": "/tmp/tmpfr39cfkg/nautobot-app-floor-plan", "dir": "", "branch_prefix": "drift-manager", "context": { "codeowner_github_usernames": "\\", "full_name": "Network to Code, LLC", "email": "info@networktocode.com", "github_org": "nautobot", "app_name": "nautobot_floor_plan", "verbose_name": "Nautobot Floor Plan", "app_slug": "nautobot-floor-plan", "project_slug": "nautobot-app-floor-plan", "repo_url": "https://github.com/nautobot/nautobot-app-floor-plan", "base_url": "floor-plan", "min_nautobot_version": "2.0.0", "max_nautobot_version": "2.9999", "camel_name": "FloorPlan", "project_short_description": "Nautobot Floor Plan", "model_class_name": "FloorPlan", "open_source_license": "Apache-2.0", "docs_base_url": "https://docs.nautobot.com", "docs_app_url": "https://docs.nautobot.com/projects/floor-plan/en/latest", "_template": "https://github.com/nautobot/cookiecutter-nautobot-app.git", "_output_dir": "/tmp/tmpfr39cfkg", "_repo_dir": "/github/home/.cookiecutters/cookiecutter-nautobot-app/nautobot-app", "_checkout": "refs/tags/nautobot-app-v2.1.0" }, "base_branch": "develop", "remote_name": "origin", "pull_request_strategy": "PullRequestStrategy.CREATE", "post_actions": [ "PostAction.BLACK" ], "baked_commit_ref": "649b2933f7e719c03e16ac9d694db63e994f4409", "draft": true } ``` CLI Arguments: ``` { "cookie_dir": "", "input": false, "json_filename": "", "output_dir": "", "push": true, "template": "", "template_dir": "", "template_ref": "refs/tags/nautobot-app-v2.1.0", "pull_request": null, "post_action": [], "disable_post_actions": false, "draft": null } ``` --- .cookiecutter.json | 4 +- .../pull_request_template.md | 2 +- .github/workflows/ci.yml | 51 +++++- .github/workflows/rebake.yml | 118 ------------- LICENSE | 2 +- README.md | 8 +- changes/.gitignore | 1 + development/Dockerfile | 4 +- development/docker-compose.base.yml | 6 +- development/docker-compose.dev.yml | 12 ++ development/towncrier_template.j2 | 30 ++++ docs/admin/compatibility_matrix.md | 8 +- docs/admin/install.md | 15 +- docs/admin/release_notes/version_1.0.md | 18 +- docs/admin/uninstall.md | 6 + docs/admin/upgrade.md | 3 + docs/assets/extra.css | 5 + docs/dev/arch_decision.md | 7 + docs/dev/contributing.md | 31 +++- docs/dev/dev_environment.md | 7 +- docs/user/app_overview.md | 8 +- nautobot_floor_plan/__init__.py | 4 +- nautobot_floor_plan/api/serializers.py | 6 +- nautobot_floor_plan/api/views.py | 10 +- nautobot_floor_plan/filters.py | 37 +--- nautobot_floor_plan/forms.py | 62 ++----- nautobot_floor_plan/models.py | 56 ++----- nautobot_floor_plan/navigation.py | 43 ++--- nautobot_floor_plan/tables.py | 19 +-- .../floorplan_retrieve.html | 56 +++---- nautobot_floor_plan/tests/fixtures.py | 13 +- nautobot_floor_plan/tests/test_api_views.py | 14 +- nautobot_floor_plan/tests/test_basic.py | 12 -- .../tests/test_filter_floorplan.py | 27 +++ .../tests/test_form_floorplan.py | 32 ++++ .../tests/test_model_floorplan.py | 22 +++ nautobot_floor_plan/tests/test_views.py | 25 ++- nautobot_floor_plan/urls.py | 32 +--- nautobot_floor_plan/views.py | 27 +-- pyproject.toml | 115 +++++++++++-- tasks.py | 158 +++++++++++++++--- 41 files changed, 621 insertions(+), 495 deletions(-) delete mode 100644 .github/workflows/rebake.yml create mode 100644 changes/.gitignore create mode 100644 development/towncrier_template.j2 create mode 100644 docs/dev/arch_decision.md create mode 100644 nautobot_floor_plan/tests/test_filter_floorplan.py create mode 100644 nautobot_floor_plan/tests/test_form_floorplan.py create mode 100644 nautobot_floor_plan/tests/test_model_floorplan.py diff --git a/.cookiecutter.json b/.cookiecutter.json index d26406e..3a9e06a 100644 --- a/.cookiecutter.json +++ b/.cookiecutter.json @@ -21,7 +21,7 @@ "_drift_manager": { "template": "https://github.com/nautobot/cookiecutter-nautobot-app.git", "template_dir": "nautobot-app", - "template_ref": "develop", + "template_ref": "refs/tags/nautobot-app-v2.1.0", "cookie_dir": "", "branch_prefix": "drift-manager", "pull_request_strategy": "create", @@ -29,7 +29,7 @@ "black" ], "draft": true, - "baked_commit_ref": "649b2933f7e719c03e16ac9d694db63e994f4409" + "baked_commit_ref": "bfb0956d3c47a62a4f47d3d3d67cd0f5c4325d1d" } } } \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md index b1eba07..9085806 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -1,5 +1,5 @@ " +issue_format = "[#{issue}](https://github.com/nautobot/nautobot-app-floor-plan/issues/{issue})" + +[[tool.towncrier.type]] +directory = "security" +name = "Security" +showcontent = true + +[[tool.towncrier.type]] +directory = "added" +name = "Added" +showcontent = true + +[[tool.towncrier.type]] +directory = "changed" +name = "Changed" +showcontent = true + +[[tool.towncrier.type]] +directory = "deprecated" +name = "Deprecated" +showcontent = true + +[[tool.towncrier.type]] +directory = "removed" +name = "Removed" +showcontent = true + +[[tool.towncrier.type]] +directory = "fixed" +name = "Fixed" +showcontent = true + +[[tool.towncrier.type]] +directory = "dependencies" +name = "Dependencies" +showcontent = true + +[[tool.towncrier.type]] +directory = "documentation" +name = "Documentation" +showcontent = true + +[[tool.towncrier.type]] +directory = "housekeeping" +name = "Housekeeping" +showcontent = true diff --git a/tasks.py b/tasks.py index e24e08c..1498548 100644 --- a/tasks.py +++ b/tasks.py @@ -13,6 +13,8 @@ """ import os +from pathlib import Path +from time import sleep from invoke.collection import Collection from invoke.tasks import task as invoke_task @@ -21,7 +23,8 @@ def is_truthy(arg): """Convert "truthy" strings into Booleans. - Examples: + Examples + -------- >>> is_truthy('yes') True Args: @@ -67,6 +70,25 @@ def _is_compose_included(context, name): return f"docker-compose.{name}.yml" in context.nautobot_floor_plan.compose_files +def _await_healthy_service(context, service): + container_id = docker_compose(context, f"ps -q -- {service}", pty=False, echo=False, hide=True).stdout.strip() + _await_healthy_container(context, container_id) + + +def _await_healthy_container(context, container_id): + while True: + result = context.run( + "docker inspect --format='{{.State.Health.Status}}' " + container_id, + pty=False, + echo=False, + hide=True, + ) + if result.stdout.strip() == "healthy": + break + print(f"Waiting for `{container_id}` container to become healthy ...") + sleep(1) + + def task(function=None, *args, **kwargs): """Task decorator to override the default Invoke task decorator and add each task to the invoke namespace.""" @@ -90,6 +112,7 @@ def docker_compose(context, command, **kwargs): """Helper function for running a specific docker compose command with all appropriate parameters and environment. Args: + ---- context (obj): Used to run specific commands command (str): Command string to append to the "docker compose ..." command, such as "build", "up", etc. **kwargs: Passed through to the context.run() call. @@ -216,11 +239,46 @@ def stop(context, service=""): docker_compose(context, "stop" if service else "down --remove-orphans", service=service) -@task -def destroy(context): +@task( + aliases=("down",), + help={ + "volumes": "Remove Docker compose volumes (default: True)", + "import-db-file": "Import database from `import-db-file` file into the fresh environment (default: empty)", + }, +) +def destroy(context, volumes=True, import_db_file=""): """Destroy all containers and volumes.""" print("Destroying Nautobot...") - docker_compose(context, "down --remove-orphans --volumes") + docker_compose(context, f"down --remove-orphans {'--volumes' if volumes else ''}") + + if not import_db_file: + return + + if not volumes: + raise ValueError("Cannot specify `--no-volumes` and `--import-db-file` arguments at the same time.") + + print(f"Importing database file: {import_db_file}...") + + input_path = Path(import_db_file).absolute() + if not input_path.is_file(): + raise ValueError(f"File not found: {input_path}") + + command = [ + "run", + "--rm", + "--detach", + f"--volume='{input_path}:/docker-entrypoint-initdb.d/dump.sql'", + "--", + "db", + ] + + container_id = docker_compose(context, " ".join(command), pty=False, echo=False, hide=True).stdout.strip() + _await_healthy_container(context, container_id) + print("Stopping database container...") + context.run(f"docker stop {container_id}", pty=False, echo=False, hide=True) + + print("Database import complete, you can start Nautobot with the following command:") + print("invoke start") @task @@ -424,27 +482,43 @@ def dbshell(context, db_name="", input_file="", output_file="", query=""): @task( help={ + "db-name": "Database name to create (default: Nautobot database)", "input-file": "SQL dump file to replace the existing database with. This can be generated using `invoke backup-db` (default: `dump.sql`).", } ) -def import_db(context, input_file="dump.sql"): - """Stop Nautobot containers and replace the current database with the dump into the running `db` container.""" - docker_compose(context, "stop -- nautobot worker") +def import_db(context, db_name="", input_file="dump.sql"): + """Stop Nautobot containers and replace the current database with the dump into `db` container.""" + docker_compose(context, "stop -- nautobot worker beat") + start(context, "db") + _await_healthy_service(context, "db") command = ["exec -- db sh -c '"] if _is_compose_included(context, "mysql"): + if not db_name: + db_name = "$MYSQL_DATABASE" command += [ + "mysql --user root --password=$MYSQL_ROOT_PASSWORD", + '--execute="', + f"DROP DATABASE IF EXISTS {db_name};", + f"CREATE DATABASE {db_name};", + "" + if db_name == "$MYSQL_DATABASE" + else f"GRANT ALL PRIVILEGES ON {db_name}.* TO $MYSQL_USER; FLUSH PRIVILEGES;", + '"', + "&&", "mysql", - "--database=$MYSQL_DATABASE", + f"--database={db_name}", "--user=$MYSQL_USER", "--password=$MYSQL_PASSWORD", ] elif _is_compose_included(context, "postgres"): + if not db_name: + db_name = "$POSTGRES_DB" command += [ - "psql", - "--username=$POSTGRES_USER", - "postgres", + f"dropdb --if-exists --user=$POSTGRES_USER {db_name} &&", + f"createdb --user=$POSTGRES_USER {db_name} &&", + f"psql --user=$POSTGRES_USER --dbname={db_name}", ] else: raise ValueError("Unsupported database backend.") @@ -467,7 +541,10 @@ def import_db(context, input_file="dump.sql"): } ) def backup_db(context, db_name="", output_file="dump.sql", readable=True): - """Dump database into `output_file` file from running `db` container.""" + """Dump database into `output_file` file from `db` container.""" + start(context, "db") + _await_healthy_service(context, "db") + command = ["exec -- db sh -c '"] if _is_compose_included(context, "mysql"): @@ -475,17 +552,12 @@ def backup_db(context, db_name="", output_file="dump.sql", readable=True): "mysqldump", "--user=root", "--password=$MYSQL_ROOT_PASSWORD", - "--add-drop-database", "--skip-extended-insert" if readable else "", - "--databases", db_name if db_name else "$MYSQL_DATABASE", ] elif _is_compose_included(context, "postgres"): command += [ "pg_dump", - "--clean", - "--create", - "--if-exists", "--username=$POSTGRES_USER", f"--dbname={db_name or '$POSTGRES_DB'}", "--inserts" if readable else "", @@ -542,6 +614,19 @@ def help_task(context): context.run(f"invoke {task_name} --help") +@task( + help={ + "version": "Version of Nautobot Floor Plan to generate the release notes for.", + } +) +def generate_release_notes(context, version=""): + """Generate Release Notes using Towncrier.""" + command = "env DJANGO_SETTINGS_MODULE=nautobot.core.settings towncrier build" + if version: + command += f" --version {version}" + run_command(context, command) + + # ------------------------------------------------------------------------------ # TESTS # ------------------------------------------------------------------------------ @@ -583,12 +668,34 @@ def pylint(context): run_command(context, command) -@task -def pydocstyle(context): - """Run pydocstyle to validate docstring formatting adheres to NTC defined standards.""" - # We exclude the /migrations/ directory since it is autogenerated code - command = "pydocstyle ." - run_command(context, command) +@task(aliases=("a",)) +def autoformat(context): + """Run code autoformatting.""" + black(context, autoformat=True) + ruff(context, action="both", fix=True) + + +@task( + help={ + "action": "One of 'lint', 'format', or 'both'", + "fix": "Automatically fix selected action. May not be able to fix all.", + "output_format": "see https://docs.astral.sh/ruff/settings/#output-format", + }, +) +def ruff(context, action="lint", fix=False, output_format="text"): + """Run ruff to perform code formatting and/or linting.""" + if action != "lint": + command = "ruff format" + if not fix: + command += " --check" + command += " ." + run_command(context, command) + if action != "format": + command = "ruff check" + if fix: + command += " --fix" + command += f" --output-format {output_format} ." + run_command(context, command) @task @@ -603,6 +710,7 @@ def yamllint(context): """Run yamllint to validate formatting adheres to NTC defined YAML standards. Args: + ---- context (obj): Used to run specific commands """ command = "yamllint . --format standard" @@ -691,12 +799,12 @@ def tests(context, failfast=False, keepdb=False, lint_only=False): # Sorted loosely from fastest to slowest print("Running black...") black(context) + print("Running ruff...") + ruff(context) print("Running flake8...") flake8(context) print("Running bandit...") bandit(context) - print("Running pydocstyle...") - pydocstyle(context) print("Running yamllint...") yamllint(context) print("Running poetry check...") From dc777a031ee4a5d998bef58b5b9f378d44149a9e Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Tue, 30 Jan 2024 12:59:04 +0000 Subject: [PATCH 2/9] fix: Manual drift fixes --- .github/workflows/ci.yml | 2 +- README.md | 6 +- docs/admin/compatibility_matrix.md | 8 +-- docs/admin/install.md | 15 +---- docs/admin/release_notes/version_1.0.md | 18 +----- docs/admin/upgrade.md | 3 - docs/dev/arch_decision.md | 7 --- docs/user/app_overview.md | 8 ++- nautobot_floor_plan/api/views.py | 6 ++ nautobot_floor_plan/filters.py | 37 ++++++++++-- nautobot_floor_plan/forms.py | 60 +++++++++++++++---- nautobot_floor_plan/models.py | 56 ++++++++++++----- nautobot_floor_plan/navigation.py | 43 +++++++------ nautobot_floor_plan/tables.py | 19 +++--- .../floorplan_retrieve.html | 56 ++++++++++------- nautobot_floor_plan/tests/fixtures.py | 13 +++- nautobot_floor_plan/tests/test_api_views.py | 14 +---- .../tests/test_filter_floorplan.py | 27 --------- .../tests/test_form_floorplan.py | 32 ---------- .../tests/test_model_floorplan.py | 22 ------- nautobot_floor_plan/tests/test_views.py | 25 ++++---- nautobot_floor_plan/urls.py | 28 +++++++-- nautobot_floor_plan/views.py | 23 ++++++- pyproject.toml | 2 + 24 files changed, 293 insertions(+), 237 deletions(-) delete mode 100644 docs/dev/arch_decision.md delete mode 100644 nautobot_floor_plan/tests/test_filter_floorplan.py delete mode 100644 nautobot_floor_plan/tests/test_form_floorplan.py delete mode 100644 nautobot_floor_plan/tests/test_model_floorplan.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d0ee2e..f9cdbd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: "Check out repository code" uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v4" + uses: "networktocode/gh-action-setup-poetry-environment@v5" - name: "Linting: ruff" run: "poetry run invoke ruff" check-docs-build: diff --git a/README.md b/README.md index 962672a..018a78f 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,11 @@ This App aids in data center management by providing the ability to create a gri ![A sample floor plan](https://raw.githubusercontent.com/nautobot/nautobot-app-floor-plan/develop/docs/images/floor-plan-populated.png) -> Developer Note: Place the files in the `docs/images/` folder and link them using only full URLs from GitHub, for example: `![Overview](https://raw.githubusercontent.com/nautobot/nautobot-app-floor-plan/develop/docs/images/app-overview.png)`. This absolute static linking is required to ensure the README renders properly in GitHub, the docs site, and any other external sites like PyPI. +![Button to add a new floor plan](https://raw.githubusercontent.com/nautobot/nautobot-app-floor-plan/develop/docs/images/add-floor-plan-button.png) -More screenshots can be found in the [Using the App](https://docs.nautobot.com/projects/floor-plan/en/latest/user/app_use_cases/) page in the documentation. Here's a quick overview of some of the app's added functionality: +![Form to define a new floor plan](https://raw.githubusercontent.com/nautobot/nautobot-app-floor-plan/develop/docs/images/add-floor-plan-form.png) -![](https://raw.githubusercontent.com/nautobot/nautobot-app-floor-plan/develop/docs/images/placeholder.png) +![A new blank floor plan](https://raw.githubusercontent.com/nautobot/nautobot-app-floor-plan/develop/docs/images/floor-plan-empty.png) More screenshots can be found in the [Using the App](https://docs.nautobot.com/projects/floor-plan/en/latest/user/app_use_cases/) page in the documentation. diff --git a/docs/admin/compatibility_matrix.md b/docs/admin/compatibility_matrix.md index e32075d..63e59b1 100644 --- a/docs/admin/compatibility_matrix.md +++ b/docs/admin/compatibility_matrix.md @@ -1,8 +1,8 @@ # Compatibility Matrix -!!! warning "Developer Note - Remove Me!" - Explain how the release models of the app and of Nautobot work together, how releases are supported, how features and older releases are deprecated etc. +Because this App depends on Nautobot's `Location` data model, which was introduced in Nautobot 1.4.0, the initial release of this App requires at least Nautobot 1.4.0. It should work with all later Nautobot 1.x.y versions. | Nautobot Floor Plan Version | Nautobot First Support Version | Nautobot Last Support Version | -| ------------- | -------------------- | ------------- | -| 1.0.X | 2.0.0 | 2.99.99 | +| --------------------------- | ------------------------------ | ----------------------------- | +| 1.0.X | 1.4.0 | 1.99.99 | +| 2.0.X | 2.0.0 | 2.99.99 | diff --git a/docs/admin/install.md b/docs/admin/install.md index 21a79de..566873c 100644 --- a/docs/admin/install.md +++ b/docs/admin/install.md @@ -13,7 +13,7 @@ Here you will find detailed instructions on how to **install** and **configure** ## Install Guide !!! note - Apps can be installed manually or using Python's `pip`. See the [nautobot documentation](https://nautobot.readthedocs.io/en/latest/plugins/#install-the-package) for more details. The pip package name for this app is [`nautobot-floor-plan`](https://pypi.org/project/nautobot-floor-plan/). + Apps can be installed manually or using Python's `pip`. See the [Nautobot documentation](https://nautobot.readthedocs.io/en/latest/plugins/#install-the-package) for more details. The pip package name for this app is [`nautobot-floor-plan`](https://pypi.org/project/nautobot-floor-plan/). The app is available as a Python package via PyPI and can be installed with `pip`: @@ -52,15 +52,4 @@ Then restart the Nautobot services which may include: sudo systemctl restart nautobot nautobot-worker nautobot-scheduler ``` -## App Configuration - -!!! warning "Developer Note - Remove Me!" - Any configuration required to get the App set up. Edit the table below as per the examples provided. - -The app behavior can be controlled with the following list of settings: - -| Key | Example | Default | Description | -| ------- | ------ | -------- | ------------------------------------- | -| `enable_backup` | `True` | `True` | A boolean to represent whether or not to run backup configurations within the app. | -| `platform_slug_map` | `{"cisco_wlc": "cisco_aireos"}` | `None` | A dictionary in which the key is the platform slug and the value is what netutils uses in any "network_os" parameter. | -| `per_feature_bar_width` | `0.15` | `0.15` | The width of the table bar within the overview report | +If the App has been installed successfully, the Nautobot web UI should now show a new "Location Floor Plans" menu item under the "Organization" menu. diff --git a/docs/admin/release_notes/version_1.0.md b/docs/admin/release_notes/version_1.0.md index 530ac79..e0c9f68 100644 --- a/docs/admin/release_notes/version_1.0.md +++ b/docs/admin/release_notes/version_1.0.md @@ -4,24 +4,10 @@ This document describes all new features and changes in the release `1.0`. The f ## Release Overview -- Major features or milestones -- Achieved in this `x.y` release -- Changes to compatibility with Nautobot and/or other apps, libraries etc. +- Initial public release. ## [v1.0.0] - 2023-MM-DD ### Added -### Changed - -### Fixed - -- [#123](https://github.com/nautobot/nautobot-app-floor-plan/issues/123) Fixed Tag filtering not working in job launch form - -## [v1.0.0] - 2021-08-03 - -### Added - -### Changed - -### Fixed +- Initial public release. diff --git a/docs/admin/upgrade.md b/docs/admin/upgrade.md index 6c6a3f3..836ab18 100644 --- a/docs/admin/upgrade.md +++ b/docs/admin/upgrade.md @@ -4,7 +4,4 @@ Here you will find any steps necessary to upgrade the App in your Nautobot envir ## Upgrade Guide -!!! warning "Developer Note - Remove Me!" - Add more detailed steps on how the app is upgraded in an existing Nautobot setup and any version specifics (such as upgrading between major versions with breaking changes). - When a new release comes out it may be necessary to run a migration of the database to account for any changes in the data models used by this app. Execute the command `nautobot-server post-upgrade` within the runtime environment of your Nautobot installation after updating the `nautobot-floor-plan` package via `pip`. diff --git a/docs/dev/arch_decision.md b/docs/dev/arch_decision.md deleted file mode 100644 index 6a68035..0000000 --- a/docs/dev/arch_decision.md +++ /dev/null @@ -1,7 +0,0 @@ -# Architecture Decision Records - -The intention is to document deviations from a standard Model View Controller (MVC) design. - -!!! warning "Developer Note - Remove Me!" - Optional page, remove if not applicable. - For examples see [Golden Config](https://github.com/nautobot/nautobot-plugin-golden-config/tree/develop/docs/dev/dev_adr.md). diff --git a/docs/user/app_overview.md b/docs/user/app_overview.md index a52765a..ab0be77 100644 --- a/docs/user/app_overview.md +++ b/docs/user/app_overview.md @@ -36,8 +36,12 @@ Included is a non-exhaustive list of capabilites beyond a standard MVC (model vi ## Nautobot Features Used -!!! warning "Developer Note - Remove Me!" - What is shown today in the Installed Apps page in Nautobot. What parts of Nautobot does it interact with, what does it add etc. ? +This App: + +- Adds a "Location Floor Plans" menu item to Nautobot's "Organization" menu. +- Adds two new database models, "Floor Plan" and "Floor Plan Tile". +- Adds UI and REST API endpoints for performing standard create/retrieve/update/delete (CRUD) operations on these models. +- Extends the detail view of Nautobot Locations to include an "Add/Remove Floor Plan" button and (when a Floor Plan is defined) a "Floor Plan" tab to display and interact with the rendered floor plan. ### Extras diff --git a/nautobot_floor_plan/api/views.py b/nautobot_floor_plan/api/views.py index 14afff9..fa58750 100644 --- a/nautobot_floor_plan/api/views.py +++ b/nautobot_floor_plan/api/views.py @@ -1,5 +1,11 @@ """API views for nautobot_floor_plan.""" +from django.http import HttpResponse +from django.shortcuts import get_object_or_404 +from django.views.decorators.clickjacking import xframe_options_sameorigin +from drf_spectacular.utils import extend_schema +from rest_framework.decorators import action + from nautobot.apps.api import NautobotModelViewSet from nautobot_floor_plan import filters, models diff --git a/nautobot_floor_plan/filters.py b/nautobot_floor_plan/filters.py index 597e4ff..b8e9eef 100644 --- a/nautobot_floor_plan/filters.py +++ b/nautobot_floor_plan/filters.py @@ -1,11 +1,15 @@ """Filtering for nautobot_floor_plan.""" -from nautobot.apps.filters import NautobotFilterSet, NameSearchFilterSet +import django_filters + +from nautobot.dcim.models import Location, Rack +from nautobot.apps.filters import NautobotFilterSet +from nautobot.apps.filters import NaturalKeyOrPKMultipleChoiceFilter, SearchFilter from nautobot_floor_plan import models -class FloorPlanFilterSet(NautobotFilterSet, NameSearchFilterSet): # pylint: disable=too-many-ancestors +class FloorPlanFilterSet(NautobotFilterSet): """Filter for FloorPlan.""" q = SearchFilter( @@ -24,5 +28,30 @@ class Meta: model = models.FloorPlan fields = ["x_size", "y_size", "tile_width", "tile_depth", "tags"] - # add any fields from the model that you would like to filter your searches by using those - fields = ["id", "name", "description"] + +class FloorPlanTileFilterSet(NautobotFilterSet): + """Filter for FloorPlanTile.""" + + q = SearchFilter( + filter_predicates={ + "floor_plan__location__name": "icontains", + "rack__name": "icontains", + }, + ) + floor_plan = django_filters.ModelMultipleChoiceFilter(queryset=models.FloorPlan.objects.all()) + location = NaturalKeyOrPKMultipleChoiceFilter( + field_name="floor_plan__location", + queryset=Location.objects.all(), + label="Location (name or ID)", + ) + rack = NaturalKeyOrPKMultipleChoiceFilter( + queryset=Rack.objects.all(), + to_field_name="name", + label="Rack (name or ID)", + ) + + class Meta: + """Meta attributes.""" + + model = models.FloorPlanTile + fields = ["x_origin", "y_origin", "tags"] diff --git a/nautobot_floor_plan/forms.py b/nautobot_floor_plan/forms.py index eb56c20..37bf9b8 100644 --- a/nautobot_floor_plan/forms.py +++ b/nautobot_floor_plan/forms.py @@ -4,25 +4,41 @@ """Forms for nautobot_floor_plan.""" from django import forms -from nautobot.apps.forms import NautobotBulkEditForm, NautobotFilterForm, NautobotModelForm, TagsBulkEditFormMixin + +from nautobot.dcim.models import Location, Rack +from nautobot.apps.forms import ( + NautobotBulkEditForm, + NautobotFilterForm, + NautobotModelForm, + TagsBulkEditFormMixin, + DynamicModelChoiceField, + DynamicModelMultipleChoiceField, + TagFilterField, +) from nautobot_floor_plan import models -class FloorPlanForm(NautobotModelForm): # pylint: disable=too-many-ancestors +class FloorPlanForm(NautobotModelForm): """FloorPlan creation/edit form.""" + location = DynamicModelChoiceField(queryset=Location.objects.all()) + class Meta: """Meta attributes.""" model = models.FloorPlan fields = [ - "name", - "description", + "location", + "x_size", + "y_size", + "tile_width", + "tile_depth", + "tags", ] -class FloorPlanBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): # pylint: disable=too-many-ancestors +class FloorPlanBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): """FloorPlan bulk edit form.""" pk = forms.ModelMultipleChoiceField(queryset=models.FloorPlan.objects.all(), widget=forms.MultipleHiddenInput) @@ -41,11 +57,33 @@ class FloorPlanFilterForm(NautobotFilterForm): """Filter form to filter searches.""" model = models.FloorPlan - field_order = ["q", "name"] + field_order = ["q", "location", "x_size", "y_size"] + + q = forms.CharField(required=False, label="Search") + location = DynamicModelMultipleChoiceField(queryset=Location.objects.all(), to_field_name="pk", required=False) + tag = TagFilterField(model) - q = forms.CharField( - required=False, - label="Search", - help_text="Search within Name or Slug.", + +class FloorPlanTileForm(NautobotModelForm): + """FloorPlanTile creation/edit form.""" + + floor_plan = DynamicModelChoiceField(queryset=models.FloorPlan.objects.all()) + rack = DynamicModelChoiceField( + queryset=Rack.objects.all(), required=False, query_params={"nautobot_floor_plan_floor_plan": "$floor_plan"} ) - name = forms.CharField(required=False, label="Name") + + class Meta: + """Meta attributes.""" + + model = models.FloorPlanTile + fields = [ + "floor_plan", + "x_origin", + "y_origin", + "x_size", + "y_size", + "status", + "rack", + "rack_orientation", + "tags", + ] diff --git a/nautobot_floor_plan/models.py b/nautobot_floor_plan/models.py index 2e0f647..774c2ac 100644 --- a/nautobot_floor_plan/models.py +++ b/nautobot_floor_plan/models.py @@ -6,32 +6,58 @@ from django.core.validators import MinValueValidator from django.db import models -# Nautobot imports +from nautobot.apps.models import extras_features from nautobot.apps.models import PrimaryModel +from nautobot.apps.models import StatusField + +from nautobot_floor_plan.choices import RackOrientationChoices +from nautobot_floor_plan.svg import FloorPlanSVG logger = logging.getLogger(__name__) -# If you want to choose a specific model to overload in your class declaration, please reference the following documentation: -# how to chose a database model: https://docs.nautobot.com/projects/core/en/stable/plugins/development/#database-models -class FloorPlan(PrimaryModel): # pylint: disable=too-many-ancestors - """Base model for Nautobot Floor Plan app.""" +@extras_features( + "custom_fields", + # "custom_links", Not really needed since this doesn't have distinct views as compared to a Location. + "custom_validators", + "export_templates", + "graphql", + "relationships", + "webhooks", +) +class FloorPlan(PrimaryModel): + """ + Model representing the floor plan of a given Location. - name = models.CharField(max_length=100, unique=True) - description = models.CharField(max_length=200, blank=True) - # additional model fields + Within a FloorPlan, individual areas are defined as FloorPlanTile records. + """ - class Meta: - """Metaclass attributes.""" + location = models.OneToOneField(to="dcim.Location", on_delete=models.CASCADE, related_name="floor_plan") - ordering = ["name"] + x_size = models.PositiveSmallIntegerField( + validators=[MinValueValidator(1)], + help_text='Absolute width of the floor plan, in "tiles"', + ) + y_size = models.PositiveSmallIntegerField( + validators=[MinValueValidator(1)], + help_text='Absolute depth of the floor plan, in "tiles"', + ) + tile_width = models.PositiveSmallIntegerField( + validators=[MinValueValidator(1)], + default=100, + help_text='Relative width of each "tile" in the floor plan (cm, inches, etc.)', + ) + tile_depth = models.PositiveSmallIntegerField( + validators=[MinValueValidator(1)], + default=100, + help_text='Relative depth of each "tile" in the floor plan (cm, inches, etc.)', + ) - # Option for fixing capitalization (i.e. "Snmp" vs "SNMP") - # verbose_name = "Nautobot Floor Plan" + class Meta: + """Metaclass attributes.""" - # Option for fixing plural name (i.e. "Chicken Tenders" vs "Chicken Tendies") - # verbose_name_plural = "Nautobot Floor Plans" + ordering = ["location___name"] def __str__(self): """Stringify instance.""" diff --git a/nautobot_floor_plan/navigation.py b/nautobot_floor_plan/navigation.py index b6116a3..9a56c8b 100644 --- a/nautobot_floor_plan/navigation.py +++ b/nautobot_floor_plan/navigation.py @@ -1,25 +1,32 @@ """Menu items.""" -from nautobot.apps.ui import NavMenuAddButton, NavMenuGroup, NavMenuItem, NavMenuTab - - -items = ( - NavMenuItem( - link="plugins:nautobot_floor_plan:floorplan_list", - name="Nautobot Floor Plan", - permissions=["nautobot_floor_plan.view_floorplan"], - buttons=( - NavMenuAddButton( - link="plugins:nautobot_floor_plan:floorplan_add", - permissions=["nautobot_floor_plan.add_floorplan"], - ), - ), - ), -) +from nautobot.apps.ui import NavMenuGroup, NavMenuItem, NavMenuTab, NavMenuAddButton, NavMenuImportButton menu_items = ( NavMenuTab( - name="Apps", - groups=(NavMenuGroup(name="Nautobot Floor Plan", items=tuple(items)),), + name="Organization", + groups=( + NavMenuGroup( + name="Locations", + items=( + NavMenuItem( + name="Location Floor Plans", + link="plugins:nautobot_floor_plan:floorplan_list", + weight=300, + permissions=["nautobot_floor_plan.view_floorplan"], + buttons=( + NavMenuAddButton( + link="plugins:nautobot_floor_plan:floorplan_add", + permissions=["nautobot_floor_plan.add_floorplan"], + ), + NavMenuImportButton( + link="plugins:nautobot_floor_plan:floorplan_import", + permissions=["nautobot_floor_plan.add_floorplan"], + ), + ), + ), + ), + ), + ), ), ) diff --git a/nautobot_floor_plan/tables.py b/nautobot_floor_plan/tables.py index a126b60..263fb64 100644 --- a/nautobot_floor_plan/tables.py +++ b/nautobot_floor_plan/tables.py @@ -1,7 +1,8 @@ """Tables for nautobot_floor_plan.""" import django_tables2 as tables -from nautobot.apps.tables import BaseTable, ButtonsColumn, ToggleColumn +from nautobot.apps.tables import BaseTable, ButtonsColumn, TagColumn, ToggleColumn +from nautobot.core.templatetags.helpers import hyperlinked_object from nautobot_floor_plan import models @@ -11,14 +12,14 @@ class FloorPlanTable(BaseTable): """Table for list view.""" pk = ToggleColumn() - name = tables.Column(linkify=True) - actions = ButtonsColumn( - models.FloorPlan, - # Option for modifying the default action buttons on each row: - # buttons=("changelog", "edit", "delete"), - # Option for modifying the pk for the action buttons: - pk_field="pk", - ) + floor_plan = tables.Column(empty_values=[]) + location = tables.Column(linkify=True) + tags = TagColumn() + actions = ButtonsColumn(models.FloorPlan) + + def render_floor_plan(self, record): + """Render a link to the detail view for the FloorPlan record itself.""" + return hyperlinked_object(record) class Meta(BaseTable.Meta): """Meta attributes.""" diff --git a/nautobot_floor_plan/templates/nautobot_floor_plan/floorplan_retrieve.html b/nautobot_floor_plan/templates/nautobot_floor_plan/floorplan_retrieve.html index 16e89da..ea84b17 100644 --- a/nautobot_floor_plan/templates/nautobot_floor_plan/floorplan_retrieve.html +++ b/nautobot_floor_plan/templates/nautobot_floor_plan/floorplan_retrieve.html @@ -1,26 +1,42 @@ - -{% extends 'generic/object_retrieve.html' %} +{% extends "generic/object_retrieve.html" %} {% load helpers %} +{% block breadcrumbs %} +
  • Locations
  • +
  • {{ object.location | hyperlinked_object }}
  • +
  • {{ object | hyperlinked_object }}
  • +{% endblock breadcrumbs %} + {% block content_left_page %}
    - FloorPlan -
    - - - - - - - - - -
    Name - {{ object.name }} -
    Description - {{ object.description|placeholder }} -
    -
    -{% endblock %} + Floor Plan + + + + + + + + + + + + + + + + + + + + + + +
    Location{{ object.location | hyperlinked_object }}
    X Size (Tiles){{ object.x_size }}
    Y Size (Tiles){{ object.y_size }}
    Tile Width (Relative Units){{ object.tile_width }}
    Tile Depth (Relative Units){{ object.tile_depth }}
    + +{% endblock content_left_page %} +{% block content_full_width_page %} + {% include 'nautobot_floor_plan/inc/floorplan_svg.html' with floor_plan=object %} +{% endblock content_full_width_page %} diff --git a/nautobot_floor_plan/tests/fixtures.py b/nautobot_floor_plan/tests/fixtures.py index 82c376e..d7202b4 100644 --- a/nautobot_floor_plan/tests/fixtures.py +++ b/nautobot_floor_plan/tests/fixtures.py @@ -39,6 +39,13 @@ def create_prerequisites(floor_count=4): def create_floor_plans(locations): """Fixture to create necessary number of FloorPlan for tests.""" - FloorPlan.objects.create(name="Test One") - FloorPlan.objects.create(name="Test Two") - FloorPlan.objects.create(name="Test Three") + size = 1 + floor_plans = [] + + for location in locations: + floor_plan = FloorPlan(location=location, x_size=size, y_size=size) + floor_plan.validated_save() + floor_plans.append(floor_plan) + size += 1 + + return floor_plans diff --git a/nautobot_floor_plan/tests/test_api_views.py b/nautobot_floor_plan/tests/test_api_views.py index 91d4791..5a68e25 100644 --- a/nautobot_floor_plan/tests/test_api_views.py +++ b/nautobot_floor_plan/tests/test_api_views.py @@ -1,5 +1,5 @@ """Unit tests for nautobot_floor_plan.""" -from nautobot.apps.testing import APIViewTestCases +from django.contrib.contenttypes.models import ContentType from nautobot.dcim.models import Rack from nautobot.extras.models import Tag @@ -13,16 +13,8 @@ class FloorPlanAPIViewTest(APIViewTestCases.APIViewTestCase): """Test the API viewsets for FloorPlan.""" model = models.FloorPlan - create_data = [ - { - "name": "Test Model 1", - "description": "test description", - }, - { - "name": "Test Model 2", - }, - ] - bulk_update_data = {"description": "Test Bulk Update"} + bulk_update_data = {"x_size": 10, "y_size": 1} + brief_fields = ["display", "id", "url", "x_size", "y_size"] @classmethod def setUpTestData(cls): diff --git a/nautobot_floor_plan/tests/test_filter_floorplan.py b/nautobot_floor_plan/tests/test_filter_floorplan.py deleted file mode 100644 index 01a0024..0000000 --- a/nautobot_floor_plan/tests/test_filter_floorplan.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Test FloorPlan Filter.""" -from django.test import TestCase -from nautobot_floor_plan import filters -from nautobot_floor_plan import models -from nautobot_floor_plan.tests import fixtures - - -class FloorPlanFilterTestCase(TestCase): - """FloorPlan Filter Test Case.""" - - queryset = models.FloorPlan.objects.all() - filterset = filters.FloorPlanFilterSet - - @classmethod - def setUpTestData(cls): - """Setup test data for FloorPlan Model.""" - fixtures.create_floorplan() - - def test_q_search_name(self): - """Test using Q search with name of FloorPlan.""" - params = {"q": "Test One"} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - - def test_q_invalid(self): - """Test using invalid Q search for FloorPlan.""" - params = {"q": "test-five"} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 0) diff --git a/nautobot_floor_plan/tests/test_form_floorplan.py b/nautobot_floor_plan/tests/test_form_floorplan.py deleted file mode 100644 index 9b0f304..0000000 --- a/nautobot_floor_plan/tests/test_form_floorplan.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Test floorplan forms.""" -from django.test import TestCase - -from nautobot_floor_plan import forms - - -class FloorPlanTest(TestCase): - """Test FloorPlan forms.""" - - def test_specifying_all_fields_success(self): - form = forms.FloorPlanForm( - data={ - "name": "Development", - "description": "Development Testing", - } - ) - self.assertTrue(form.is_valid()) - self.assertTrue(form.save()) - - def test_specifying_only_required_success(self): - form = forms.FloorPlanForm( - data={ - "name": "Development", - } - ) - self.assertTrue(form.is_valid()) - self.assertTrue(form.save()) - - def test_validate_name_floorplan_is_required(self): - form = forms.FloorPlanForm(data={"description": "Development Testing"}) - self.assertFalse(form.is_valid()) - self.assertIn("This field is required.", form.errors["name"]) diff --git a/nautobot_floor_plan/tests/test_model_floorplan.py b/nautobot_floor_plan/tests/test_model_floorplan.py deleted file mode 100644 index 105768f..0000000 --- a/nautobot_floor_plan/tests/test_model_floorplan.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Test FloorPlan.""" - -from django.test import TestCase - -from nautobot_floor_plan import models - - -class TestFloorPlan(TestCase): - """Test FloorPlan.""" - - def test_create_floorplan_only_required(self): - """Create with only required fields, and validate null description and __str__.""" - floorplan = models.FloorPlan.objects.create(name="Development") - self.assertEqual(floorplan.name, "Development") - self.assertEqual(floorplan.description, "") - self.assertEqual(str(floorplan), "Development") - - def test_create_floorplan_all_fields_success(self): - """Create FloorPlan with all fields.""" - floorplan = models.FloorPlan.objects.create(name="Development", description="Development Test") - self.assertEqual(floorplan.name, "Development") - self.assertEqual(floorplan.description, "Development Test") diff --git a/nautobot_floor_plan/tests/test_views.py b/nautobot_floor_plan/tests/test_views.py index 48a2cc8..1bba6fb 100644 --- a/nautobot_floor_plan/tests/test_views.py +++ b/nautobot_floor_plan/tests/test_views.py @@ -1,4 +1,5 @@ """Unit tests for views.""" + from nautobot.apps.testing import ViewTestCases from nautobot_floor_plan import models @@ -9,18 +10,22 @@ class FloorPlanViewTest(ViewTestCases.PrimaryObjectViewTestCase): """Test the FloorPlan views.""" model = models.FloorPlan - bulk_edit_data = {"description": "Bulk edit views"} - form_data = { - "name": "Test 1", - "description": "Initial model", - } + bulk_edit_data = {"x_size": 10, "y_size": 10, "tile_width": 200, "tile_depth": 200} csv_data = ( - "name", - "Test csv1", - "Test csv2", - "Test csv3", + "location__name,x_size,y_size,tile_width,tile_depth", + "Floor 4,1,2,100,100", + "Floor 5,2,4,100,200", + "Floor 6,3,6,200,100", ) @classmethod def setUpTestData(cls): - fixtures.create_floorplan() + data = fixtures.create_prerequisites(floor_count=6) + fixtures.create_floor_plans(data["floors"][:3]) + cls.form_data = { + "location": data["floors"][3].pk, + "tile_depth": 100, + "tile_width": 100, + "x_size": 1, + "y_size": 2, + } diff --git a/nautobot_floor_plan/urls.py b/nautobot_floor_plan/urls.py index 725493d..dc5fd43 100644 --- a/nautobot_floor_plan/urls.py +++ b/nautobot_floor_plan/urls.py @@ -1,10 +1,30 @@ """Django urlpatterns declaration for nautobot_floor_plan app.""" + +from django.urls import path + from nautobot.apps.urls import NautobotUIViewSetRouter +from nautobot.extras.views import ObjectChangeLogView, ObjectNotesView -from nautobot_floor_plan import views +from nautobot_floor_plan import models, views +app_name = "floor_plan" router = NautobotUIViewSetRouter() -router.register("floorplan", views.FloorPlanUIViewSet) - -urlpatterns = router.urls +router.register("floor-plans", views.FloorPlanUIViewSet) +router.register("floor-plan-tiles", views.FloorPlanTileUIViewSet) +urlpatterns = [ + path( + "models//changelog/", + ObjectChangeLogView.as_view(), + name="floorplan_changelog", + kwargs={"model": models.FloorPlan}, + ), + path( + "models//notes/", + ObjectNotesView.as_view(), + name="floorplan_notes", + kwargs={"model": models.FloorPlan}, + ), + path("locations//floor_plan/", views.LocationFloorPlanTab.as_view(), name="location_floor_plan_tab"), +] +urlpatterns += router.urls diff --git a/nautobot_floor_plan/views.py b/nautobot_floor_plan/views.py index fd8f4a5..914d38b 100644 --- a/nautobot_floor_plan/views.py +++ b/nautobot_floor_plan/views.py @@ -1,11 +1,14 @@ -"""Views for nautobot_floor_plan.""" +"""Views for FloorPlan.""" + from nautobot.apps.views import NautobotUIViewSet +from nautobot.apps.views import ObjectView +from nautobot.dcim.models import Location from nautobot_floor_plan import filters, forms, models, tables from nautobot_floor_plan.api import serializers -class FloorPlanUIViewSet(NautobotUIViewSet): +class FloorPlanUIViewSet(NautobotUIViewSet): # TODO we only need a subset of views """ViewSet for FloorPlan views.""" bulk_update_form_class = forms.FloorPlanBulkEditForm @@ -16,3 +19,19 @@ class FloorPlanUIViewSet(NautobotUIViewSet): queryset = models.FloorPlan.objects.all() serializer_class = serializers.FloorPlanSerializer table_class = tables.FloorPlanTable + + +class LocationFloorPlanTab(ObjectView): + """Add a "Floor Plan" tab to the Location detail view.""" + + queryset = Location.objects.all() + template_name = "nautobot_floor_plan/location_floor_plan.html" + + +class FloorPlanTileUIViewSet(NautobotUIViewSet): # TODO we only need a subset of views + """ViewSet for FloorPlanTile views.""" + + form_class = forms.FloorPlanTileForm + lookup_field = "pk" + queryset = models.FloorPlanTile.objects.all() + serializer_class = serializers.FloorPlanTileSerializer diff --git a/pyproject.toml b/pyproject.toml index 12ab1b7..a833b3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,8 @@ packages = [ { include = "nautobot_floor_plan" }, ] include = [ + "LICENSE", + "README.md", # Poetry by default will exclude files that are in .gitignore "nautobot_floor_plan/static/nautobot_floor_plan/docs/**/*", ] From 1cbcdae17b12f2ac069e42189b00dd9df2d25218 Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Tue, 30 Jan 2024 12:59:24 +0000 Subject: [PATCH 3/9] chore: Poetry lock --- poetry.lock | 115 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 85 insertions(+), 30 deletions(-) diff --git a/poetry.lock b/poetry.lock index 22eccca..ec9b125 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "amqp" @@ -467,6 +467,23 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "click-default-group" +version = "1.2.4" +description = "click_default_group" +optional = false +python-versions = ">=2.7" +files = [ + {file = "click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f"}, + {file = "click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e"}, +] + +[package.dependencies] +click = "*" + +[package.extras] +test = ["pytest"] + [[package]] name = "click-didyoumean" version = "0.3.0" @@ -1309,6 +1326,21 @@ zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +[[package]] +name = "incremental" +version = "22.10.0" +description = "\"A small library that versions your Python projects.\"" +optional = false +python-versions = "*" +files = [ + {file = "incremental-22.10.0-py2.py3-none-any.whl", hash = "sha256:b864a1f30885ee72c5ac2835a761b8fe8aa9c28b9395cacf27286602688d3e51"}, + {file = "incremental-22.10.0.tar.gz", hash = "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0"}, +] + +[package.extras] +mypy = ["click (>=6.0)", "mypy (==0.812)", "twisted (>=16.4.0)"] +scripts = ["click (>=6.0)", "twisted (>=16.4.0)"] + [[package]] name = "inflection" version = "0.5.1" @@ -2214,23 +2246,6 @@ files = [ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] -[[package]] -name = "pydocstyle" -version = "6.3.0" -description = "Python docstring style checker" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, - {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, -] - -[package.dependencies] -snowballstemmer = ">=2.2.0" - -[package.extras] -toml = ["tomli (>=1.2.3)"] - [[package]] name = "pyflakes" version = "2.3.1" @@ -2772,6 +2787,32 @@ typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9 [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "ruff" +version = "0.1.15" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, + {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, + {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, + {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, + {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, +] + [[package]] name = "rx" version = "1.6.3" @@ -2835,17 +2876,6 @@ files = [ {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, ] -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -optional = false -python-versions = "*" -files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] - [[package]] name = "social-auth-app-django" version = "5.2.0" @@ -2973,6 +3003,28 @@ files = [ {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, ] +[[package]] +name = "towncrier" +version = "23.6.0" +description = "Building newsfiles for your project." +optional = false +python-versions = ">=3.7" +files = [ + {file = "towncrier-23.6.0-py3-none-any.whl", hash = "sha256:da552f29192b3c2b04d630133f194c98e9f14f0558669d427708e203fea4d0a5"}, + {file = "towncrier-23.6.0.tar.gz", hash = "sha256:fc29bd5ab4727c8dacfbe636f7fb5dc53b99805b62da1c96b214836159ff70c1"}, +] + +[package.dependencies] +click = "*" +click-default-group = "*" +importlib-resources = {version = ">=5", markers = "python_version < \"3.10\""} +incremental = "*" +jinja2 = "*" +tomli = {version = "*", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["furo", "packaging", "sphinx (>=5)", "twisted"] + [[package]] name = "traitlets" version = "5.9.0" @@ -3216,7 +3268,10 @@ files = [ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +[extras] +all = [] + [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.12" -content-hash = "1722f65bb223b35d263bbda220a7fc821c6df96ea9b7d6ee9aa315447cdc06ca" +content-hash = "35058482f7e5ba1a106313d95f246bf80ce458b3b3df411bc08d3db88f50d6b7" From 157a107136308ca36d4c71b1982d01982ac19a81 Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Tue, 30 Jan 2024 13:14:44 +0000 Subject: [PATCH 4/9] fix: Added changelog fragment --- changes/67.changed | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/67.changed diff --git a/changes/67.changed b/changes/67.changed new file mode 100644 index 0000000..7a7cdde --- /dev/null +++ b/changes/67.changed @@ -0,0 +1 @@ +Replaced pydocstyle with ruff. From 4ce36a7e0eb30badd9ede8dfccc4c22d5e2259b1 Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Wed, 31 Jan 2024 09:33:07 +0000 Subject: [PATCH 5/9] fix: Do not polute site-packages --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a833b3a..12ab1b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,8 +21,6 @@ packages = [ { include = "nautobot_floor_plan" }, ] include = [ - "LICENSE", - "README.md", # Poetry by default will exclude files that are in .gitignore "nautobot_floor_plan/static/nautobot_floor_plan/docs/**/*", ] From fe6fd5e62daa37f048ddc08080536e619651496f Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Wed, 31 Jan 2024 10:01:41 +0000 Subject: [PATCH 6/9] fix: RTD badge link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 018a78f..2970a0e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@
    - +
    From 949cf98a5ecef2a4068c88ecd2bb2e2d4fd66999 Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Wed, 31 Jan 2024 10:39:47 +0000 Subject: [PATCH 7/9] fix: Trailing slash in docs URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2970a0e..f09cb09 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@
    - +
    From 1de3e238d98584b2baaacc40a3534ff370080f9f Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Wed, 31 Jan 2024 10:40:04 +0000 Subject: [PATCH 8/9] fix: Revert unnecessary tasks changes --- tasks.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tasks.py b/tasks.py index 1498548..b9ec2cf 100644 --- a/tasks.py +++ b/tasks.py @@ -23,8 +23,7 @@ def is_truthy(arg): """Convert "truthy" strings into Booleans. - Examples - -------- + Examples: >>> is_truthy('yes') True Args: @@ -112,7 +111,6 @@ def docker_compose(context, command, **kwargs): """Helper function for running a specific docker compose command with all appropriate parameters and environment. Args: - ---- context (obj): Used to run specific commands command (str): Command string to append to the "docker compose ..." command, such as "build", "up", etc. **kwargs: Passed through to the context.run() call. @@ -710,7 +708,6 @@ def yamllint(context): """Run yamllint to validate formatting adheres to NTC defined YAML standards. Args: - ---- context (obj): Used to run specific commands """ command = "yamllint . --format standard" From 5cebebc7f86cd142a84640a62c218794a6cd1969 Mon Sep 17 00:00:00 2001 From: Jan Snasel Date: Wed, 31 Jan 2024 10:40:20 +0000 Subject: [PATCH 9/9] fix: Remove obsolete task `lock_poetry` --- tasks.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tasks.py b/tasks.py index b9ec2cf..0f34811 100644 --- a/tasks.py +++ b/tasks.py @@ -722,20 +722,6 @@ def check_migrations(context): run_command(context, command) -@task( - help={ - "check": "Whether to run poetry check or lock (defaults to False)", - } -) -def lock_poetry(context, check=False): - """Generate poetry.lock, or run poetry check.""" - command = [ - "poetry", - "check" if check else "lock --no-update", - ] - run_command(context, " ".join(command)) - - @task( help={ "keepdb": "save and re-use test database between test runs for faster re-testing.",