From 459b0deb8604507fbce211c454bb7248d4406b81 Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Thu, 21 Mar 2024 17:23:55 +0100 Subject: [PATCH 01/16] Add usage of letters for rendering FP SVG. --- nautobot_floor_plan/choices.py | 12 ++++++++++ nautobot_floor_plan/forms.py | 2 ++ .../migrations/0004_auto_20240321_1438.py | 22 +++++++++++++++++++ nautobot_floor_plan/models.py | 14 +++++++++++- nautobot_floor_plan/svg.py | 20 ++++++++++++++--- .../floorplan_retrieve.html | 8 +++++++ nautobot_floor_plan/tests/test_api_views.py | 1 + nautobot_floor_plan/tests/test_forms.py | 21 +++++++++++++++--- nautobot_floor_plan/tests/test_views.py | 4 +++- 9 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 nautobot_floor_plan/migrations/0004_auto_20240321_1438.py diff --git a/nautobot_floor_plan/choices.py b/nautobot_floor_plan/choices.py index cd0fd7f..5b8a775 100644 --- a/nautobot_floor_plan/choices.py +++ b/nautobot_floor_plan/choices.py @@ -17,3 +17,15 @@ class RackOrientationChoices(ChoiceSet): (LEFT, "left"), (RIGHT, "right"), ) + + +class AxisLabelsChoices(ChoiceSet): + """Choices for grid numbering style.""" + + NUMBERS = "numbers" + LETTERS = "letters" + + CHOICES = ( + (NUMBERS, "Numbers"), + (LETTERS, "Letters"), + ) diff --git a/nautobot_floor_plan/forms.py b/nautobot_floor_plan/forms.py index 37bf9b8..f389829 100644 --- a/nautobot_floor_plan/forms.py +++ b/nautobot_floor_plan/forms.py @@ -34,6 +34,8 @@ class Meta: "y_size", "tile_width", "tile_depth", + "x_axis_labels", + "y_axis_labels", "tags", ] diff --git a/nautobot_floor_plan/migrations/0004_auto_20240321_1438.py b/nautobot_floor_plan/migrations/0004_auto_20240321_1438.py new file mode 100644 index 0000000..46ac173 --- /dev/null +++ b/nautobot_floor_plan/migrations/0004_auto_20240321_1438.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.21 on 2024-03-21 14:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("nautobot_floor_plan", "0003_auto_20230908_1339"), + ] + + operations = [ + migrations.AddField( + model_name="floorplan", + name="x_axis_labels", + field=models.CharField(default="numbers", max_length=10), + ), + migrations.AddField( + model_name="floorplan", + name="y_axis_labels", + field=models.CharField(default="numbers", max_length=10), + ), + ] diff --git a/nautobot_floor_plan/models.py b/nautobot_floor_plan/models.py index 774c2ac..96f0df2 100644 --- a/nautobot_floor_plan/models.py +++ b/nautobot_floor_plan/models.py @@ -10,7 +10,7 @@ from nautobot.apps.models import PrimaryModel from nautobot.apps.models import StatusField -from nautobot_floor_plan.choices import RackOrientationChoices +from nautobot_floor_plan.choices import RackOrientationChoices, AxisLabelsChoices from nautobot_floor_plan.svg import FloorPlanSVG @@ -53,6 +53,18 @@ class FloorPlan(PrimaryModel): default=100, help_text='Relative depth of each "tile" in the floor plan (cm, inches, etc.)', ) + x_axis_labels = models.CharField( + max_length=10, + choices=AxisLabelsChoices, + default=AxisLabelsChoices.NUMBERS, + help_text="Grid numbering of X axis (horizontal).", + ) + y_axis_labels = models.CharField( + max_length=10, + choices=AxisLabelsChoices, + default=AxisLabelsChoices.NUMBERS, + help_text="Grid numbering of Y axis (vertical).", + ) class Meta: """Metaclass attributes.""" diff --git a/nautobot_floor_plan/svg.py b/nautobot_floor_plan/svg.py index d31623e..5d7bef9 100644 --- a/nautobot_floor_plan/svg.py +++ b/nautobot_floor_plan/svg.py @@ -9,7 +9,7 @@ from nautobot.core.templatetags.helpers import fgcolor -from nautobot_floor_plan.choices import RackOrientationChoices +from nautobot_floor_plan.choices import RackOrientationChoices, AxisLabelsChoices logger = logging.getLogger(__name__) @@ -81,6 +81,18 @@ def _setup_drawing(self, width, depth): return drawing + @staticmethod + def _col_num_to_letter(col_num): + col_str = "" + while col_num: + remainder = col_num % 26 + if remainder == 0: + remainder = 26 + col_letter = chr(ord("A") + remainder - 1) + col_str = col_letter + col_str + col_num = int((col_num - 1) / 26) + return col_str + def _draw_grid(self, drawing): """Render the grid underlying all tiles.""" # Vertical lines @@ -109,9 +121,10 @@ def _draw_grid(self, drawing): ) # Axis labels for x in range(1, self.floor_plan.x_size + 1): + label = self._col_num_to_letter(x) if self.floor_plan.x_axis_labels == AxisLabelsChoices.LETTERS else x drawing.add( drawing.text( - str(x), + label, insert=( (x - 0.5) * self.GRID_SIZE_X + self.GRID_OFFSET, self.BORDER_WIDTH + self.TEXT_LINE_HEIGHT / 2, @@ -120,9 +133,10 @@ def _draw_grid(self, drawing): ) ) for y in range(1, self.floor_plan.y_size + 1): + label = self._col_num_to_letter(y) if self.floor_plan.y_axis_labels == AxisLabelsChoices.LETTERS else y drawing.add( drawing.text( - str(y), + label, insert=( self.BORDER_WIDTH + self.TEXT_LINE_HEIGHT / 2, (y - 0.5) * self.GRID_SIZE_Y + self.GRID_OFFSET, 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 ea84b17..31186ed 100644 --- a/nautobot_floor_plan/templates/nautobot_floor_plan/floorplan_retrieve.html +++ b/nautobot_floor_plan/templates/nautobot_floor_plan/floorplan_retrieve.html @@ -33,6 +33,14 @@ Tile Depth (Relative Units) {{ object.tile_depth }} + + X Axis Numbering + {{ object.x_axis_labels }} + + + Y Axis Numbering + {{ object.y_axis_labels }} + {% endblock content_left_page %} diff --git a/nautobot_floor_plan/tests/test_api_views.py b/nautobot_floor_plan/tests/test_api_views.py index 5a68e25..6808b57 100644 --- a/nautobot_floor_plan/tests/test_api_views.py +++ b/nautobot_floor_plan/tests/test_api_views.py @@ -15,6 +15,7 @@ class FloorPlanAPIViewTest(APIViewTestCases.APIViewTestCase): model = models.FloorPlan bulk_update_data = {"x_size": 10, "y_size": 1} brief_fields = ["display", "id", "url", "x_size", "y_size"] + choices_fields = ["x_axis_labels", "y_axis_labels"] @classmethod def setUpTestData(cls): diff --git a/nautobot_floor_plan/tests/test_forms.py b/nautobot_floor_plan/tests/test_forms.py index 1a50cba..ae6f23d 100644 --- a/nautobot_floor_plan/tests/test_forms.py +++ b/nautobot_floor_plan/tests/test_forms.py @@ -5,7 +5,7 @@ from nautobot.extras.models import Tag from nautobot.core.testing import TestCase -from nautobot_floor_plan import forms, models +from nautobot_floor_plan import forms, models, choices from nautobot_floor_plan.tests import fixtures @@ -20,7 +20,15 @@ def setUp(self): def test_valid_minimal_inputs(self): """Test creation with minimal input data.""" form = forms.FloorPlanForm( - data={"location": self.floors[0].pk, "x_size": 1, "y_size": 2, "tile_depth": 100, "tile_width": 200} + data={ + "location": self.floors[0].pk, + "x_size": 1, + "y_size": 2, + "tile_depth": 100, + "tile_width": 200, + "x_axis_labels": choices.AxisLabelsChoices.NUMBERS, + "y_axis_labels": choices.AxisLabelsChoices.NUMBERS, + } ) self.assertTrue(form.is_valid()) form.save() @@ -41,6 +49,8 @@ def test_valid_extra_inputs(self): "y_size": 2, "tile_depth": 1, "tile_width": 2, + "x_axis_labels": choices.AxisLabelsChoices.NUMBERS, + "y_axis_labels": choices.AxisLabelsChoices.NUMBERS, "tags": [tag], } ) @@ -51,12 +61,17 @@ def test_valid_extra_inputs(self): self.assertEqual(floor_plan.y_size, 2) self.assertEqual(floor_plan.tile_width, 2) self.assertEqual(floor_plan.tile_depth, 1) + self.assertEqual(floor_plan.x_axis_labels, choices.AxisLabelsChoices.NUMBERS) + self.assertEqual(floor_plan.y_axis_labels, choices.AxisLabelsChoices.NUMBERS) self.assertEqual(list(floor_plan.tags.all()), [tag]) def test_invalid_required_fields(self): """Test form validation with missing required fields.""" form = forms.FloorPlanForm(data={}) self.assertFalse(form.is_valid()) - self.assertEqual(["location", "tile_depth", "tile_width", "x_size", "y_size"], sorted(form.errors.keys())) + self.assertEqual( + ["location", "tile_depth", "tile_width", "x_axis_labels", "x_size", "y_axis_labels", "y_size"], + sorted(form.errors.keys()), + ) for message in form.errors.values(): self.assertIn("This field is required.", message) diff --git a/nautobot_floor_plan/tests/test_views.py b/nautobot_floor_plan/tests/test_views.py index 1bba6fb..fd2c77e 100644 --- a/nautobot_floor_plan/tests/test_views.py +++ b/nautobot_floor_plan/tests/test_views.py @@ -2,7 +2,7 @@ from nautobot.apps.testing import ViewTestCases -from nautobot_floor_plan import models +from nautobot_floor_plan import models, choices from nautobot_floor_plan.tests import fixtures @@ -28,4 +28,6 @@ def setUpTestData(cls): "tile_width": 100, "x_size": 1, "y_size": 2, + "x_axis_labels": choices.AxisLabelsChoices.NUMBERS, + "y_axis_labels": choices.AxisLabelsChoices.NUMBERS, } From 4a249e27162bbfcef724199988b50979acbef421 Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Fri, 22 Mar 2024 16:06:06 +0100 Subject: [PATCH 02/16] Support letters in tile form. --- nautobot_floor_plan/forms.py | 32 +++++++++++++++++- nautobot_floor_plan/svg.py | 23 ++++--------- nautobot_floor_plan/tests/test_utils.py | 45 +++++++++++++++++++++++++ nautobot_floor_plan/utils.py | 22 ++++++++++++ 4 files changed, 105 insertions(+), 17 deletions(-) create mode 100644 nautobot_floor_plan/tests/test_utils.py create mode 100644 nautobot_floor_plan/utils.py diff --git a/nautobot_floor_plan/forms.py b/nautobot_floor_plan/forms.py index f389829..0b3dcf2 100644 --- a/nautobot_floor_plan/forms.py +++ b/nautobot_floor_plan/forms.py @@ -14,9 +14,10 @@ DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField, + add_blank_choice, ) -from nautobot_floor_plan import models +from nautobot_floor_plan import models, choices, utils class FloorPlanForm(NautobotModelForm): @@ -48,6 +49,8 @@ class FloorPlanBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): y_size = forms.IntegerField(min_value=1, required=False) tile_width = forms.IntegerField(min_value=1, required=False) tile_depth = forms.IntegerField(min_value=1, required=False) + x_axis_labels = forms.ChoiceField(choices=add_blank_choice(choices.AxisLabelsChoices), required=False) + y_axis_labels = forms.ChoiceField(choices=add_blank_choice(choices.AxisLabelsChoices), required=False) class Meta: """Meta attributes.""" @@ -73,6 +76,7 @@ class FloorPlanTileForm(NautobotModelForm): rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, query_params={"nautobot_floor_plan_floor_plan": "$floor_plan"} ) + _char = forms.CharField() # temp char field for replacement in __init__ if grid numbering is using letters. class Meta: """Meta attributes.""" @@ -89,3 +93,29 @@ class Meta: "rack_orientation", "tags", ] + + def __init__(self, *args, **kwargs): + """Overwrite the constructor to swap x/y origins fields to CharField if letters are used for grid numbering.""" + super().__init__(*args, **kwargs) + self.x_letters = False + self.y_letters = False + char_field = self.fields.pop("_char") + + if fp_id := self.initial["floor_plan"] or self.data["floor_plan"]: + fp_obj = self.fields["floor_plan"].queryset.get(id=fp_id) + if fp_obj.x_axis_labels == choices.AxisLabelsChoices.LETTERS: + self.x_letters = True + self.fields["x_origin"] = char_field + if fp_obj.y_axis_labels == choices.AxisLabelsChoices.LETTERS: + self.y_letters = True + self.fields["y_origin"] = char_field + + def clean(self): + """Change x/y origins back to numbers.""" + if self.x_letters: + self.cleaned_data["x_origin"] = utils.column_letter_to_num(self.cleaned_data.get("x_origin")) + + if self.y_letters: + self.cleaned_data["y_origin"] = utils.column_letter_to_num(self.cleaned_data.get("y_origin")) + + return super().clean() diff --git a/nautobot_floor_plan/svg.py b/nautobot_floor_plan/svg.py index 5d7bef9..9bb0b7e 100644 --- a/nautobot_floor_plan/svg.py +++ b/nautobot_floor_plan/svg.py @@ -10,6 +10,7 @@ from nautobot.core.templatetags.helpers import fgcolor from nautobot_floor_plan.choices import RackOrientationChoices, AxisLabelsChoices +from nautobot_floor_plan.utils import col_num_to_letter logger = logging.getLogger(__name__) @@ -81,18 +82,6 @@ def _setup_drawing(self, width, depth): return drawing - @staticmethod - def _col_num_to_letter(col_num): - col_str = "" - while col_num: - remainder = col_num % 26 - if remainder == 0: - remainder = 26 - col_letter = chr(ord("A") + remainder - 1) - col_str = col_letter + col_str - col_num = int((col_num - 1) / 26) - return col_str - def _draw_grid(self, drawing): """Render the grid underlying all tiles.""" # Vertical lines @@ -121,7 +110,7 @@ def _draw_grid(self, drawing): ) # Axis labels for x in range(1, self.floor_plan.x_size + 1): - label = self._col_num_to_letter(x) if self.floor_plan.x_axis_labels == AxisLabelsChoices.LETTERS else x + label = col_num_to_letter(x) if self.floor_plan.x_axis_labels == AxisLabelsChoices.LETTERS else str(x) drawing.add( drawing.text( label, @@ -133,7 +122,7 @@ def _draw_grid(self, drawing): ) ) for y in range(1, self.floor_plan.y_size + 1): - label = self._col_num_to_letter(y) if self.floor_plan.y_axis_labels == AxisLabelsChoices.LETTERS else y + label = col_num_to_letter(y) if self.floor_plan.y_axis_labels == AxisLabelsChoices.LETTERS else str(y) drawing.add( drawing.text( label, @@ -146,13 +135,15 @@ def _draw_grid(self, drawing): ) # Links to populate tiles + y_letters = self.floor_plan.y_axis_labels == AxisLabelsChoices.LETTERS + x_letters = self.floor_plan.x_axis_labels == AxisLabelsChoices.LETTERS for y in range(1, self.floor_plan.y_size + 1): for x in range(1, self.floor_plan.x_size + 1): query_params = urlencode( { "floor_plan": self.floor_plan.pk, - "x_origin": x, - "y_origin": y, + "x_origin": col_num_to_letter(x) if x_letters else x, + "y_origin": col_num_to_letter(y) if y_letters else y, "return_url": self.return_url, } ) diff --git a/nautobot_floor_plan/tests/test_utils.py b/nautobot_floor_plan/tests/test_utils.py new file mode 100644 index 0000000..1bed841 --- /dev/null +++ b/nautobot_floor_plan/tests/test_utils.py @@ -0,0 +1,45 @@ +"""Test floorplan utils.""" + +import unittest + +from nautobot_floor_plan import utils + + +class TestUtils(unittest.TestCase): + """Test class.""" + + def test_col_num_to_letter(self): + test_cases = [ + (1, "A"), + (26, "Z"), + (27, "AA"), + (52, "AZ"), + (53, "BA"), + (78, "BZ"), + (79, "CA"), + (104, "CZ"), + (703, "AAA"), + (18278, "ZZZ"), + ] + + for num, expected in test_cases: + with self.subTest(num=num): + self.assertEqual(utils.col_num_to_letter(num), expected) + + def test_column_letter_to_num(self): + test_cases = [ + ("A", 1), + ("Z", 26), + ("AA", 27), + ("AZ", 52), + ("BA", 53), + ("BZ", 78), + ("CA", 79), + ("CZ", 104), + ("AAA", 703), + ("ZZZ", 18278), + ] + + for letter, expected in test_cases: + with self.subTest(letter=letter): + self.assertEqual(utils.column_letter_to_num(letter), expected) diff --git a/nautobot_floor_plan/utils.py b/nautobot_floor_plan/utils.py new file mode 100644 index 0000000..af2f807 --- /dev/null +++ b/nautobot_floor_plan/utils.py @@ -0,0 +1,22 @@ +"""Utilities module.""" + + +def col_num_to_letter(number): + """Returns letter for number [1 - 26] --> [A - Z], [27 - 52] --> [AA - AZ].""" + col_str = "" + while number: + remainder = number % 26 + if remainder == 0: + remainder = 26 + col_letter = chr(ord("A") + remainder - 1) + col_str = col_letter + col_str + number = int((number - 1) / 26) + return col_str + + +def column_letter_to_num(letter): + """Returns number for letter [A - Z] --> [1 - 26], [AA - AZ] --> [27 - 52].""" + number = ord(letter[-1]) - 64 + if letter[:-1]: + return 26 * (column_letter_to_num(letter[:-1])) + number + return number From ae3302b0f9e78ed1d302b0f57cd65072c82dc66d Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Fri, 22 Mar 2024 19:57:48 +0100 Subject: [PATCH 03/16] Simplify forms, add app config options to set defaults. --- docs/admin/install.md | 16 ++++++++++++++ nautobot_floor_plan/__init__.py | 6 +++++- nautobot_floor_plan/forms.py | 37 +++++++++++++++++++-------------- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/docs/admin/install.md b/docs/admin/install.md index 247ef89..4a5eb22 100644 --- a/docs/admin/install.md +++ b/docs/admin/install.md @@ -34,6 +34,13 @@ Once installed, the app needs to be enabled in your Nautobot configuration. The ```python # In your nautobot_config.py PLUGINS = ["nautobot_floor_plan"] + +# Optionally you can override default settings for config items to make grid numbering like a chessboard (as seen in this example) +PLUGINS_CONFIG = { + "nautobot_floor_plan": { + "grid_x_axis_labels": "letters", + } +} ``` Once the Nautobot configuration is updated, run the Post Upgrade command (`nautobot-server post_upgrade`) to run migrations and clear any cache: @@ -53,3 +60,12 @@ sudo systemctl restart nautobot nautobot-worker nautobot-scheduler ``` 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. + +## App Configuration + +The app behavior can be controlled with the following list of settings: + +| Key | Example | Default | Description | +|--------------------|-----------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| +| grid_x_axis_labels | "leters" | "numbers" | Label style for the floor plan gird. Can use numbers or letters in order. This setting will set the default selected value in the create form. | +| grid_y_axis_labels | "numbers" | "numbers" | Label style for the floor plan gird. Can use numbers or letters in order. This setting will set the default selected value in the create form. | diff --git a/nautobot_floor_plan/__init__.py b/nautobot_floor_plan/__init__.py index 65dca51..ac33c94 100644 --- a/nautobot_floor_plan/__init__.py +++ b/nautobot_floor_plan/__init__.py @@ -3,6 +3,7 @@ from importlib import metadata from nautobot.apps import NautobotAppConfig +from nautobot_floor_plan import choices __version__ = metadata.version(__name__) @@ -19,7 +20,10 @@ class FloorPlanConfig(NautobotAppConfig): required_settings = [] min_version = "2.0.0" max_version = "2.9999" - default_settings = {} + default_settings = { + "grid_x_axis_labels": choices.AxisLabelsChoices.NUMBERS, + "grid_y_axis_labels": choices.AxisLabelsChoices.NUMBERS, + } caching_config = {} diff --git a/nautobot_floor_plan/forms.py b/nautobot_floor_plan/forms.py index 0b3dcf2..54ece0e 100644 --- a/nautobot_floor_plan/forms.py +++ b/nautobot_floor_plan/forms.py @@ -16,6 +16,7 @@ TagFilterField, add_blank_choice, ) +from nautobot.apps.config import get_app_settings_or_config from nautobot_floor_plan import models, choices, utils @@ -40,6 +41,12 @@ class Meta: "tags", ] + def __init__(self, *args, **kwargs): + """Overwrite the constructor to set initial values for select widget.""" + super().__init__(*args, **kwargs) + self.initial["x_axis_labels"] = get_app_settings_or_config("nautobot_floor_plan", "grid_x_axis_labels") + self.initial["y_axis_labels"] = get_app_settings_or_config("nautobot_floor_plan", "grid_y_axis_labels") + class FloorPlanBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): """FloorPlan bulk edit form.""" @@ -76,7 +83,8 @@ class FloorPlanTileForm(NautobotModelForm): rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, query_params={"nautobot_floor_plan_floor_plan": "$floor_plan"} ) - _char = forms.CharField() # temp char field for replacement in __init__ if grid numbering is using letters. + x_origin = forms.CharField() + y_origin = forms.CharField() class Meta: """Meta attributes.""" @@ -95,27 +103,24 @@ class Meta: ] def __init__(self, *args, **kwargs): - """Overwrite the constructor to swap x/y origins fields to CharField if letters are used for grid numbering.""" + """Overwrite the constructor to define grid numbering style.""" super().__init__(*args, **kwargs) self.x_letters = False self.y_letters = False - char_field = self.fields.pop("_char") if fp_id := self.initial["floor_plan"] or self.data["floor_plan"]: fp_obj = self.fields["floor_plan"].queryset.get(id=fp_id) - if fp_obj.x_axis_labels == choices.AxisLabelsChoices.LETTERS: - self.x_letters = True - self.fields["x_origin"] = char_field - if fp_obj.y_axis_labels == choices.AxisLabelsChoices.LETTERS: - self.y_letters = True - self.fields["y_origin"] = char_field - - def clean(self): - """Change x/y origins back to numbers.""" + self.x_letters = fp_obj.x_axis_labels == choices.AxisLabelsChoices.LETTERS + self.y_letters = fp_obj.y_axis_labels == choices.AxisLabelsChoices.LETTERS + + def clean_x_origin(self): + """Convert x_origin to an integer.""" if self.x_letters: - self.cleaned_data["x_origin"] = utils.column_letter_to_num(self.cleaned_data.get("x_origin")) + return utils.column_letter_to_num(self.cleaned_data.get("x_origin")) + return int(self.cleaned_data["x_origin"]) + def clean_y_origin(self): + """Convert y_origin to an integer.""" if self.y_letters: - self.cleaned_data["y_origin"] = utils.column_letter_to_num(self.cleaned_data.get("y_origin")) - - return super().clean() + return utils.column_letter_to_num(self.cleaned_data.get("y_origin")) + return int(self.cleaned_data["y_origin"]) From fd5005a238a7802a37a9650a41c9311fb43ed1ab Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Mon, 25 Mar 2024 14:15:23 +0100 Subject: [PATCH 04/16] Add changes/79.added --- changes/79.added | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/79.added diff --git a/changes/79.added b/changes/79.added new file mode 100644 index 0000000..e87bbc1 --- /dev/null +++ b/changes/79.added @@ -0,0 +1 @@ +Added letter grid numbering. From b2bbd4a6b7a284d9c94bd72088587368863097a8 Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Mon, 25 Mar 2024 20:07:02 +0100 Subject: [PATCH 05/16] Fix forms edit, add form validation. --- nautobot_floor_plan/forms.py | 37 ++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/nautobot_floor_plan/forms.py b/nautobot_floor_plan/forms.py index 54ece0e..95021db 100644 --- a/nautobot_floor_plan/forms.py +++ b/nautobot_floor_plan/forms.py @@ -3,7 +3,10 @@ # pylint: disable=nb-incorrect-base-class """Forms for nautobot_floor_plan.""" +import re + from django import forms +from django.core.exceptions import ValidationError from nautobot.dcim.models import Location, Rack from nautobot.apps.forms import ( @@ -113,14 +116,36 @@ def __init__(self, *args, **kwargs): self.x_letters = fp_obj.x_axis_labels == choices.AxisLabelsChoices.LETTERS self.y_letters = fp_obj.y_axis_labels == choices.AxisLabelsChoices.LETTERS + if self.instance.x_origin or self.instance.y_origin: + if self.x_letters: + self.initial["x_origin"] = utils.col_num_to_letter(self.instance.x_origin) + if self.y_letters: + self.initial["x_origin"] = utils.col_num_to_letter(self.instance.y_origin) + + def letter_validator(self, value, axis): + """Validate that origin uses combination of letters.""" + if not re.search(r"[A-Z]+", value): + raise ValidationError(f"{axis} origin should use capital letters.") + + def number_validator(self, value, axis): + """Validate that origin uses combination of numbers.""" + if not re.search(r"\d+", value): + raise ValidationError(f"{axis} origin should use numbers.") + def clean_x_origin(self): - """Convert x_origin to an integer.""" + """Validate input and convert x_origin to an integer.""" + x_origin = self.cleaned_data.get("x_origin") if self.x_letters: - return utils.column_letter_to_num(self.cleaned_data.get("x_origin")) - return int(self.cleaned_data["x_origin"]) + self.letter_validator(x_origin, "X") + return utils.column_letter_to_num(x_origin) + self.number_validator(x_origin, "X") + return int(x_origin) def clean_y_origin(self): - """Convert y_origin to an integer.""" + """Validate input and convert y_origin to an integer.""" + y_origin = self.cleaned_data.get("y_origin") if self.y_letters: - return utils.column_letter_to_num(self.cleaned_data.get("y_origin")) - return int(self.cleaned_data["y_origin"]) + self.letter_validator(y_origin, "Y") + return utils.column_letter_to_num(y_origin) + self.number_validator(y_origin, "Y") + return int(y_origin) From 2a2eff25e35ab246bbe8126ad1a639caf0c0c204 Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Mon, 25 Mar 2024 20:23:01 +0100 Subject: [PATCH 06/16] Fix --- nautobot_floor_plan/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautobot_floor_plan/forms.py b/nautobot_floor_plan/forms.py index 95021db..765be12 100644 --- a/nautobot_floor_plan/forms.py +++ b/nautobot_floor_plan/forms.py @@ -120,7 +120,7 @@ def __init__(self, *args, **kwargs): if self.x_letters: self.initial["x_origin"] = utils.col_num_to_letter(self.instance.x_origin) if self.y_letters: - self.initial["x_origin"] = utils.col_num_to_letter(self.instance.y_origin) + self.initial["y_origin"] = utils.col_num_to_letter(self.instance.y_origin) def letter_validator(self, value, axis): """Validate that origin uses combination of letters.""" From 2e39c9fdf3c7dcdca3bfa9f7b88a1c9b4ad2d639 Mon Sep 17 00:00:00 2001 From: Patryk Szulczewski Date: Mon, 25 Mar 2024 20:56:01 +0100 Subject: [PATCH 07/16] Apply suggestions from code review Update from code review Co-authored-by: Joe Wesch <10467633+joewesch@users.noreply.github.com> --- changes/79.added | 2 +- docs/admin/install.md | 6 +++--- nautobot_floor_plan/models.py | 4 ++-- .../templates/nautobot_floor_plan/floorplan_retrieve.html | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/changes/79.added b/changes/79.added index e87bbc1..0dac0c0 100644 --- a/changes/79.added +++ b/changes/79.added @@ -1 +1 @@ -Added letter grid numbering. +Added an option to change the grid column/row labels to use letters rather than numbers. diff --git a/docs/admin/install.md b/docs/admin/install.md index 4a5eb22..60b5398 100644 --- a/docs/admin/install.md +++ b/docs/admin/install.md @@ -35,7 +35,7 @@ Once installed, the app needs to be enabled in your Nautobot configuration. The # In your nautobot_config.py PLUGINS = ["nautobot_floor_plan"] -# Optionally you can override default settings for config items to make grid numbering like a chessboard (as seen in this example) +# Optionally you can override default settings for config items to make grid labels like a chessboard (as seen in this example) PLUGINS_CONFIG = { "nautobot_floor_plan": { "grid_x_axis_labels": "letters", @@ -67,5 +67,5 @@ The app behavior can be controlled with the following list of settings: | Key | Example | Default | Description | |--------------------|-----------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| -| grid_x_axis_labels | "leters" | "numbers" | Label style for the floor plan gird. Can use numbers or letters in order. This setting will set the default selected value in the create form. | -| grid_y_axis_labels | "numbers" | "numbers" | Label style for the floor plan gird. Can use numbers or letters in order. This setting will set the default selected value in the create form. | +| default_x_axis_labels | "leters" | "numbers" | Label style for the floor plan gird. Can use numbers or letters in order. This setting will set the default selected value in the create form. | +| default_y_axis_labels | "numbers" | "numbers" | Label style for the floor plan gird. Can use numbers or letters in order. This setting will set the default selected value in the create form. | diff --git a/nautobot_floor_plan/models.py b/nautobot_floor_plan/models.py index 96f0df2..f9f96e0 100644 --- a/nautobot_floor_plan/models.py +++ b/nautobot_floor_plan/models.py @@ -57,13 +57,13 @@ class FloorPlan(PrimaryModel): max_length=10, choices=AxisLabelsChoices, default=AxisLabelsChoices.NUMBERS, - help_text="Grid numbering of X axis (horizontal).", + help_text="Grid labels of X axis (horizontal).", ) y_axis_labels = models.CharField( max_length=10, choices=AxisLabelsChoices, default=AxisLabelsChoices.NUMBERS, - help_text="Grid numbering of Y axis (vertical).", + help_text="Grid labels of Y axis (vertical).", ) class Meta: 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 31186ed..bdc9701 100644 --- a/nautobot_floor_plan/templates/nautobot_floor_plan/floorplan_retrieve.html +++ b/nautobot_floor_plan/templates/nautobot_floor_plan/floorplan_retrieve.html @@ -34,11 +34,11 @@ {{ object.tile_depth }} - X Axis Numbering + X Axis Labels {{ object.x_axis_labels }} - Y Axis Numbering + Y Axis Labels {{ object.y_axis_labels }} From 94b409eb47338626326e76b97af08c4c7dc227c5 Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Mon, 25 Mar 2024 21:02:28 +0100 Subject: [PATCH 08/16] Code review updates #2 --- docs/admin/install.md | 2 +- nautobot_floor_plan/__init__.py | 4 ++-- nautobot_floor_plan/forms.py | 12 ++++++------ nautobot_floor_plan/svg.py | 10 +++++----- nautobot_floor_plan/tests/test_utils.py | 8 ++++---- nautobot_floor_plan/utils.py | 6 +++--- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/admin/install.md b/docs/admin/install.md index 60b5398..c78f0f9 100644 --- a/docs/admin/install.md +++ b/docs/admin/install.md @@ -38,7 +38,7 @@ PLUGINS = ["nautobot_floor_plan"] # Optionally you can override default settings for config items to make grid labels like a chessboard (as seen in this example) PLUGINS_CONFIG = { "nautobot_floor_plan": { - "grid_x_axis_labels": "letters", + "default_x_axis_labels": "letters", } } ``` diff --git a/nautobot_floor_plan/__init__.py b/nautobot_floor_plan/__init__.py index ac33c94..1d06f92 100644 --- a/nautobot_floor_plan/__init__.py +++ b/nautobot_floor_plan/__init__.py @@ -21,8 +21,8 @@ class FloorPlanConfig(NautobotAppConfig): min_version = "2.0.0" max_version = "2.9999" default_settings = { - "grid_x_axis_labels": choices.AxisLabelsChoices.NUMBERS, - "grid_y_axis_labels": choices.AxisLabelsChoices.NUMBERS, + "default_x_axis_labels": choices.AxisLabelsChoices.NUMBERS, + "default_y_axis_labels": choices.AxisLabelsChoices.NUMBERS, } caching_config = {} diff --git a/nautobot_floor_plan/forms.py b/nautobot_floor_plan/forms.py index 765be12..870de3a 100644 --- a/nautobot_floor_plan/forms.py +++ b/nautobot_floor_plan/forms.py @@ -47,8 +47,8 @@ class Meta: def __init__(self, *args, **kwargs): """Overwrite the constructor to set initial values for select widget.""" super().__init__(*args, **kwargs) - self.initial["x_axis_labels"] = get_app_settings_or_config("nautobot_floor_plan", "grid_x_axis_labels") - self.initial["y_axis_labels"] = get_app_settings_or_config("nautobot_floor_plan", "grid_y_axis_labels") + self.initial["x_axis_labels"] = get_app_settings_or_config("nautobot_floor_plan", "default_x_axis_labels") + self.initial["y_axis_labels"] = get_app_settings_or_config("nautobot_floor_plan", "default_y_axis_labels") class FloorPlanBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): @@ -118,9 +118,9 @@ def __init__(self, *args, **kwargs): if self.instance.x_origin or self.instance.y_origin: if self.x_letters: - self.initial["x_origin"] = utils.col_num_to_letter(self.instance.x_origin) + self.initial["x_origin"] = utils.num_to_letter(self.instance.x_origin) if self.y_letters: - self.initial["y_origin"] = utils.col_num_to_letter(self.instance.y_origin) + self.initial["y_origin"] = utils.num_to_letter(self.instance.y_origin) def letter_validator(self, value, axis): """Validate that origin uses combination of letters.""" @@ -137,7 +137,7 @@ def clean_x_origin(self): x_origin = self.cleaned_data.get("x_origin") if self.x_letters: self.letter_validator(x_origin, "X") - return utils.column_letter_to_num(x_origin) + return utils.letter_to_num(x_origin) self.number_validator(x_origin, "X") return int(x_origin) @@ -146,6 +146,6 @@ def clean_y_origin(self): y_origin = self.cleaned_data.get("y_origin") if self.y_letters: self.letter_validator(y_origin, "Y") - return utils.column_letter_to_num(y_origin) + return utils.letter_to_num(y_origin) self.number_validator(y_origin, "Y") return int(y_origin) diff --git a/nautobot_floor_plan/svg.py b/nautobot_floor_plan/svg.py index 9bb0b7e..bf4f15f 100644 --- a/nautobot_floor_plan/svg.py +++ b/nautobot_floor_plan/svg.py @@ -10,7 +10,7 @@ from nautobot.core.templatetags.helpers import fgcolor from nautobot_floor_plan.choices import RackOrientationChoices, AxisLabelsChoices -from nautobot_floor_plan.utils import col_num_to_letter +from nautobot_floor_plan.utils import num_to_letter logger = logging.getLogger(__name__) @@ -110,7 +110,7 @@ def _draw_grid(self, drawing): ) # Axis labels for x in range(1, self.floor_plan.x_size + 1): - label = col_num_to_letter(x) if self.floor_plan.x_axis_labels == AxisLabelsChoices.LETTERS else str(x) + label = num_to_letter(x) if self.floor_plan.x_axis_labels == AxisLabelsChoices.LETTERS else str(x) drawing.add( drawing.text( label, @@ -122,7 +122,7 @@ def _draw_grid(self, drawing): ) ) for y in range(1, self.floor_plan.y_size + 1): - label = col_num_to_letter(y) if self.floor_plan.y_axis_labels == AxisLabelsChoices.LETTERS else str(y) + label = num_to_letter(y) if self.floor_plan.y_axis_labels == AxisLabelsChoices.LETTERS else str(y) drawing.add( drawing.text( label, @@ -142,8 +142,8 @@ def _draw_grid(self, drawing): query_params = urlencode( { "floor_plan": self.floor_plan.pk, - "x_origin": col_num_to_letter(x) if x_letters else x, - "y_origin": col_num_to_letter(y) if y_letters else y, + "x_origin": num_to_letter(x) if x_letters else x, + "y_origin": num_to_letter(y) if y_letters else y, "return_url": self.return_url, } ) diff --git a/nautobot_floor_plan/tests/test_utils.py b/nautobot_floor_plan/tests/test_utils.py index 1bed841..d4ceea7 100644 --- a/nautobot_floor_plan/tests/test_utils.py +++ b/nautobot_floor_plan/tests/test_utils.py @@ -8,7 +8,7 @@ class TestUtils(unittest.TestCase): """Test class.""" - def test_col_num_to_letter(self): + def test_num_to_letter(self): test_cases = [ (1, "A"), (26, "Z"), @@ -24,9 +24,9 @@ def test_col_num_to_letter(self): for num, expected in test_cases: with self.subTest(num=num): - self.assertEqual(utils.col_num_to_letter(num), expected) + self.assertEqual(utils.num_to_letter(num), expected) - def test_column_letter_to_num(self): + def test_letter_to_num(self): test_cases = [ ("A", 1), ("Z", 26), @@ -42,4 +42,4 @@ def test_column_letter_to_num(self): for letter, expected in test_cases: with self.subTest(letter=letter): - self.assertEqual(utils.column_letter_to_num(letter), expected) + self.assertEqual(utils.letter_to_num(letter), expected) diff --git a/nautobot_floor_plan/utils.py b/nautobot_floor_plan/utils.py index af2f807..a8caa54 100644 --- a/nautobot_floor_plan/utils.py +++ b/nautobot_floor_plan/utils.py @@ -1,7 +1,7 @@ """Utilities module.""" -def col_num_to_letter(number): +def num_to_letter(number): """Returns letter for number [1 - 26] --> [A - Z], [27 - 52] --> [AA - AZ].""" col_str = "" while number: @@ -14,9 +14,9 @@ def col_num_to_letter(number): return col_str -def column_letter_to_num(letter): +def letter_to_num(letter): """Returns number for letter [A - Z] --> [1 - 26], [AA - AZ] --> [27 - 52].""" number = ord(letter[-1]) - 64 if letter[:-1]: - return 26 * (column_letter_to_num(letter[:-1])) + number + return 26 * (letter_to_num(letter[:-1])) + number return number From f94234f6afeb063eb1afc4ed5fb1668c6fb03294 Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Mon, 25 Mar 2024 21:07:01 +0100 Subject: [PATCH 09/16] Code review updates #3 update naming --- nautobot_floor_plan/forms.py | 8 ++++---- nautobot_floor_plan/svg.py | 10 +++++----- nautobot_floor_plan/tests/test_utils.py | 8 ++++---- nautobot_floor_plan/utils.py | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/nautobot_floor_plan/forms.py b/nautobot_floor_plan/forms.py index 870de3a..8db5ed1 100644 --- a/nautobot_floor_plan/forms.py +++ b/nautobot_floor_plan/forms.py @@ -118,9 +118,9 @@ def __init__(self, *args, **kwargs): if self.instance.x_origin or self.instance.y_origin: if self.x_letters: - self.initial["x_origin"] = utils.num_to_letter(self.instance.x_origin) + self.initial["x_origin"] = utils.grid_number_to_letter(self.instance.x_origin) if self.y_letters: - self.initial["y_origin"] = utils.num_to_letter(self.instance.y_origin) + self.initial["y_origin"] = utils.grid_number_to_letter(self.instance.y_origin) def letter_validator(self, value, axis): """Validate that origin uses combination of letters.""" @@ -137,7 +137,7 @@ def clean_x_origin(self): x_origin = self.cleaned_data.get("x_origin") if self.x_letters: self.letter_validator(x_origin, "X") - return utils.letter_to_num(x_origin) + return utils.grid_letter_to_number(x_origin) self.number_validator(x_origin, "X") return int(x_origin) @@ -146,6 +146,6 @@ def clean_y_origin(self): y_origin = self.cleaned_data.get("y_origin") if self.y_letters: self.letter_validator(y_origin, "Y") - return utils.letter_to_num(y_origin) + return utils.grid_letter_to_number(y_origin) self.number_validator(y_origin, "Y") return int(y_origin) diff --git a/nautobot_floor_plan/svg.py b/nautobot_floor_plan/svg.py index bf4f15f..58e69ee 100644 --- a/nautobot_floor_plan/svg.py +++ b/nautobot_floor_plan/svg.py @@ -10,7 +10,7 @@ from nautobot.core.templatetags.helpers import fgcolor from nautobot_floor_plan.choices import RackOrientationChoices, AxisLabelsChoices -from nautobot_floor_plan.utils import num_to_letter +from nautobot_floor_plan.utils import grid_number_to_letter logger = logging.getLogger(__name__) @@ -110,7 +110,7 @@ def _draw_grid(self, drawing): ) # Axis labels for x in range(1, self.floor_plan.x_size + 1): - label = num_to_letter(x) if self.floor_plan.x_axis_labels == AxisLabelsChoices.LETTERS else str(x) + label = grid_number_to_letter(x) if self.floor_plan.x_axis_labels == AxisLabelsChoices.LETTERS else str(x) drawing.add( drawing.text( label, @@ -122,7 +122,7 @@ def _draw_grid(self, drawing): ) ) for y in range(1, self.floor_plan.y_size + 1): - label = num_to_letter(y) if self.floor_plan.y_axis_labels == AxisLabelsChoices.LETTERS else str(y) + label = grid_number_to_letter(y) if self.floor_plan.y_axis_labels == AxisLabelsChoices.LETTERS else str(y) drawing.add( drawing.text( label, @@ -142,8 +142,8 @@ def _draw_grid(self, drawing): query_params = urlencode( { "floor_plan": self.floor_plan.pk, - "x_origin": num_to_letter(x) if x_letters else x, - "y_origin": num_to_letter(y) if y_letters else y, + "x_origin": grid_number_to_letter(x) if x_letters else x, + "y_origin": grid_number_to_letter(y) if y_letters else y, "return_url": self.return_url, } ) diff --git a/nautobot_floor_plan/tests/test_utils.py b/nautobot_floor_plan/tests/test_utils.py index d4ceea7..266cd42 100644 --- a/nautobot_floor_plan/tests/test_utils.py +++ b/nautobot_floor_plan/tests/test_utils.py @@ -8,7 +8,7 @@ class TestUtils(unittest.TestCase): """Test class.""" - def test_num_to_letter(self): + def test_grid_number_to_letter(self): test_cases = [ (1, "A"), (26, "Z"), @@ -24,9 +24,9 @@ def test_num_to_letter(self): for num, expected in test_cases: with self.subTest(num=num): - self.assertEqual(utils.num_to_letter(num), expected) + self.assertEqual(utils.grid_number_to_letter(num), expected) - def test_letter_to_num(self): + def test_gird_letter_to_number(self): test_cases = [ ("A", 1), ("Z", 26), @@ -42,4 +42,4 @@ def test_letter_to_num(self): for letter, expected in test_cases: with self.subTest(letter=letter): - self.assertEqual(utils.letter_to_num(letter), expected) + self.assertEqual(utils.grid_letter_to_number(letter), expected) diff --git a/nautobot_floor_plan/utils.py b/nautobot_floor_plan/utils.py index a8caa54..a9a12da 100644 --- a/nautobot_floor_plan/utils.py +++ b/nautobot_floor_plan/utils.py @@ -1,7 +1,7 @@ """Utilities module.""" -def num_to_letter(number): +def grid_number_to_letter(number): """Returns letter for number [1 - 26] --> [A - Z], [27 - 52] --> [AA - AZ].""" col_str = "" while number: @@ -14,9 +14,9 @@ def num_to_letter(number): return col_str -def letter_to_num(letter): +def grid_letter_to_number(letter): """Returns number for letter [A - Z] --> [1 - 26], [AA - AZ] --> [27 - 52].""" number = ord(letter[-1]) - 64 if letter[:-1]: - return 26 * (letter_to_num(letter[:-1])) + number + return 26 * (grid_letter_to_number(letter[:-1])) + number return number From 1158426858b0c71ebe14057aabcb710e9f8e988d Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Tue, 26 Mar 2024 14:37:32 +0100 Subject: [PATCH 10/16] Add TestFloorPlanTileForm to test_forms.py, updated validations. --- nautobot_floor_plan/forms.py | 44 +++++++------ nautobot_floor_plan/tests/test_forms.py | 84 +++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 20 deletions(-) diff --git a/nautobot_floor_plan/forms.py b/nautobot_floor_plan/forms.py index 8db5ed1..d37260f 100644 --- a/nautobot_floor_plan/forms.py +++ b/nautobot_floor_plan/forms.py @@ -86,7 +86,7 @@ class FloorPlanTileForm(NautobotModelForm): rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, query_params={"nautobot_floor_plan_floor_plan": "$floor_plan"} ) - x_origin = forms.CharField() + x_origin = forms.CharField(validators=[]) y_origin = forms.CharField() class Meta: @@ -111,7 +111,7 @@ def __init__(self, *args, **kwargs): self.x_letters = False self.y_letters = False - if fp_id := self.initial["floor_plan"] or self.data["floor_plan"]: + if fp_id := self.initial.get("floor_plan") or self.data.get("floor_plan"): fp_obj = self.fields["floor_plan"].queryset.get(id=fp_id) self.x_letters = fp_obj.x_axis_labels == choices.AxisLabelsChoices.LETTERS self.y_letters = fp_obj.y_axis_labels == choices.AxisLabelsChoices.LETTERS @@ -122,30 +122,34 @@ def __init__(self, *args, **kwargs): if self.y_letters: self.initial["y_origin"] = utils.grid_number_to_letter(self.instance.y_origin) - def letter_validator(self, value, axis): + def letter_validator(self, field, value, axis): """Validate that origin uses combination of letters.""" - if not re.search(r"[A-Z]+", value): - raise ValidationError(f"{axis} origin should use capital letters.") + if not re.search(r"[A-Z]+", str(value)): + self.add_error(field, f"{axis} origin should use capital letters.") + return False + return True - def number_validator(self, value, axis): + def number_validator(self, field, value, axis): """Validate that origin uses combination of numbers.""" - if not re.search(r"\d+", value): - raise ValidationError(f"{axis} origin should use numbers.") + if not re.search(r"\d+", str(value)): + self.add_error(field, f"{axis} origin should use numbers.") + return False + return True + + def _clean_origin(self, field_name, axis): + value = self.cleaned_data.get(field_name) + if self.x_letters and field_name == "x_origin" or self.y_letters and field_name == "y_origin": + if self.letter_validator(field_name, value, axis) is not True: + return 0 # required to pass model clean() method + return utils.grid_letter_to_number(value) + if self.number_validator(field_name, value, axis) is not True: + return 0 # required to pass model clean() method + return int(value) def clean_x_origin(self): """Validate input and convert x_origin to an integer.""" - x_origin = self.cleaned_data.get("x_origin") - if self.x_letters: - self.letter_validator(x_origin, "X") - return utils.grid_letter_to_number(x_origin) - self.number_validator(x_origin, "X") - return int(x_origin) + return self._clean_origin("x_origin", "X") def clean_y_origin(self): """Validate input and convert y_origin to an integer.""" - y_origin = self.cleaned_data.get("y_origin") - if self.y_letters: - self.letter_validator(y_origin, "Y") - return utils.grid_letter_to_number(y_origin) - self.number_validator(y_origin, "Y") - return int(y_origin) + return self._clean_origin("y_origin", "Y") diff --git a/nautobot_floor_plan/tests/test_forms.py b/nautobot_floor_plan/tests/test_forms.py index ae6f23d..54dc1ce 100644 --- a/nautobot_floor_plan/tests/test_forms.py +++ b/nautobot_floor_plan/tests/test_forms.py @@ -75,3 +75,87 @@ def test_invalid_required_fields(self): ) for message in form.errors.values(): self.assertIn("This field is required.", message) + + +class TestFloorPlanTileForm(TestCase): + """Test FloorPlanTileForm forms.""" + + def setUp(self): + """Create LocationType, Status, Location and FloorPlan records.""" + data = fixtures.create_prerequisites() + self.status = data["status"] + self.floor_plan = models.FloorPlan.objects.create( + location=data["floors"][0], + x_size=8, + y_size=8, + tile_depth=100, + tile_width=100, + x_axis_labels=choices.AxisLabelsChoices.LETTERS, + y_axis_labels=choices.AxisLabelsChoices.NUMBERS, + ) + + def test_valid_minimal_inputs(self): + """Test creation with minimal input data.""" + form = forms.FloorPlanTileForm( + data={ + "floor_plan": self.floor_plan.pk, + "x_origin": "A", + "y_origin": 1, + "x_size": 1, + "y_size": 1, + "status": self.status.pk, + } + ) + self.assertTrue(form.is_valid()) + form.save() + tile = models.FloorPlanTile.objects.get(floor_plan=self.floor_plan) + self.assertEqual(tile.floor_plan, self.floor_plan) + self.assertEqual(tile.x_origin, 1) # model uses integers. + self.assertEqual(tile.x_size, 1) + self.assertEqual(tile.y_size, 1) + self.assertEqual(tile.status, self.status) + + def test_invalid_input_with_number(self): + """Test creation with number when X axis uses letter labels.""" + form = forms.FloorPlanTileForm( + data={ + "floor_plan": self.floor_plan.pk, + "x_origin": 1, # 1 instead of "A" + "y_origin": 1, + "x_size": 1, + "y_size": 1, + "status": self.status.pk, + } + ) + self.assertFalse(form.is_valid()) + self.assertIn(["X origin should use capital letters."], form.errors.values()) + + def test_invalid_input_with_letter(self): + """Test creation with letter when Y axis uses number labels.""" + form = forms.FloorPlanTileForm( + data={ + "floor_plan": self.floor_plan.pk, + "x_origin": "A", + "y_origin": "A", # "A" instead of 1 + "x_size": 1, + "y_size": 1, + "status": self.status.pk, + } + ) + self.assertFalse(form.is_valid()) + self.assertIn(["Y origin should use numbers."], form.errors.values()) + + def test_tile_outside_of_floor_plan(self): + """Test creation with minimal input data.""" + form = forms.FloorPlanTileForm( + data={ + "floor_plan": self.floor_plan.pk, + "x_origin": "A", + "y_origin": 9, # out of range + "x_size": 1, + "y_size": 1, + "status": self.status.pk, + } + ) + self.assertFalse(form.is_valid()) + self.assertIn(['Too large for Floor Plan for Location "Floor 1"'], form.errors.values()) From a2f647e7c75566942f5812d37b35beb44655c613 Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Tue, 26 Mar 2024 14:39:44 +0100 Subject: [PATCH 11/16] Linting --- nautobot_floor_plan/forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nautobot_floor_plan/forms.py b/nautobot_floor_plan/forms.py index d37260f..c113872 100644 --- a/nautobot_floor_plan/forms.py +++ b/nautobot_floor_plan/forms.py @@ -6,7 +6,6 @@ import re from django import forms -from django.core.exceptions import ValidationError from nautobot.dcim.models import Location, Rack from nautobot.apps.forms import ( @@ -137,10 +136,11 @@ def number_validator(self, field, value, axis): return True def _clean_origin(self, field_name, axis): + """Common clean method for origin fields.""" value = self.cleaned_data.get(field_name) if self.x_letters and field_name == "x_origin" or self.y_letters and field_name == "y_origin": if self.letter_validator(field_name, value, axis) is not True: - return 0 # required to pass model clean() method + return 0 # required to pass model clean() method return utils.grid_letter_to_number(value) if self.number_validator(field_name, value, axis) is not True: return 0 # required to pass model clean() method From 6a0bf18786f119d03fb89e3bb814caba47ee84e1 Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Tue, 26 Mar 2024 14:55:29 +0100 Subject: [PATCH 12/16] Update docstring. --- nautobot_floor_plan/tests/test_forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautobot_floor_plan/tests/test_forms.py b/nautobot_floor_plan/tests/test_forms.py index 54dc1ce..0b69ebf 100644 --- a/nautobot_floor_plan/tests/test_forms.py +++ b/nautobot_floor_plan/tests/test_forms.py @@ -146,7 +146,7 @@ def test_invalid_input_with_letter(self): self.assertIn(["Y origin should use numbers."], form.errors.values()) def test_tile_outside_of_floor_plan(self): - """Test creation with minimal input data.""" + """Test a tile located outside the floor plan space.""" form = forms.FloorPlanTileForm( data={ "floor_plan": self.floor_plan.pk, From c294489144c176b71437b180420395cd646cdff9 Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Tue, 26 Mar 2024 15:53:58 +0100 Subject: [PATCH 13/16] Fix default labels. --- nautobot_floor_plan/forms.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nautobot_floor_plan/forms.py b/nautobot_floor_plan/forms.py index c113872..831ffe2 100644 --- a/nautobot_floor_plan/forms.py +++ b/nautobot_floor_plan/forms.py @@ -46,9 +46,9 @@ class Meta: def __init__(self, *args, **kwargs): """Overwrite the constructor to set initial values for select widget.""" super().__init__(*args, **kwargs) - self.initial["x_axis_labels"] = get_app_settings_or_config("nautobot_floor_plan", "default_x_axis_labels") - self.initial["y_axis_labels"] = get_app_settings_or_config("nautobot_floor_plan", "default_y_axis_labels") - + if not self.instance.created: + self.initial["x_axis_labels"] = get_app_settings_or_config("nautobot_floor_plan", "default_x_axis_labels") + self.initial["y_axis_labels"] = get_app_settings_or_config("nautobot_floor_plan", "default_y_axis_labels") class FloorPlanBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): """FloorPlan bulk edit form.""" @@ -85,7 +85,7 @@ class FloorPlanTileForm(NautobotModelForm): rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, query_params={"nautobot_floor_plan_floor_plan": "$floor_plan"} ) - x_origin = forms.CharField(validators=[]) + x_origin = forms.CharField() y_origin = forms.CharField() class Meta: From 5b062b62161591249cb6bba261c87754241638c4 Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Tue, 26 Mar 2024 15:58:50 +0100 Subject: [PATCH 14/16] Update --- docs/admin/install.md | 2 +- nautobot_floor_plan/forms.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/admin/install.md b/docs/admin/install.md index c78f0f9..d5d5181 100644 --- a/docs/admin/install.md +++ b/docs/admin/install.md @@ -67,5 +67,5 @@ The app behavior can be controlled with the following list of settings: | Key | Example | Default | Description | |--------------------|-----------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| -| default_x_axis_labels | "leters" | "numbers" | Label style for the floor plan gird. Can use numbers or letters in order. This setting will set the default selected value in the create form. | +| default_x_axis_labels | "letters" | "numbers" | Label style for the floor plan gird. Can use numbers or letters in order. This setting will set the default selected value in the create form. | | default_y_axis_labels | "numbers" | "numbers" | Label style for the floor plan gird. Can use numbers or letters in order. This setting will set the default selected value in the create form. | diff --git a/nautobot_floor_plan/forms.py b/nautobot_floor_plan/forms.py index 831ffe2..7e2b70b 100644 --- a/nautobot_floor_plan/forms.py +++ b/nautobot_floor_plan/forms.py @@ -50,6 +50,7 @@ def __init__(self, *args, **kwargs): self.initial["x_axis_labels"] = get_app_settings_or_config("nautobot_floor_plan", "default_x_axis_labels") self.initial["y_axis_labels"] = get_app_settings_or_config("nautobot_floor_plan", "default_y_axis_labels") + class FloorPlanBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm): """FloorPlan bulk edit form.""" From 8d5a26b36991d48f74302a62b21551ba6f07fd98 Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Thu, 4 Apr 2024 17:25:28 +0200 Subject: [PATCH 15/16] Post-review update --- docs/admin/install.md | 4 ++-- nautobot_floor_plan/app-config-schema.json | 18 +++++++++++++++++- nautobot_floor_plan/forms.py | 5 ++--- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/admin/install.md b/docs/admin/install.md index d5d5181..442cdb5 100644 --- a/docs/admin/install.md +++ b/docs/admin/install.md @@ -67,5 +67,5 @@ The app behavior can be controlled with the following list of settings: | Key | Example | Default | Description | |--------------------|-----------|----------|------------------------------------------------------------------------------------------------------------------------------------------------| -| default_x_axis_labels | "letters" | "numbers" | Label style for the floor plan gird. Can use numbers or letters in order. This setting will set the default selected value in the create form. | -| default_y_axis_labels | "numbers" | "numbers" | Label style for the floor plan gird. Can use numbers or letters in order. This setting will set the default selected value in the create form. | +| default_x_axis_labels | "letters" | "numbers" | Label style for the floor plan grid. Can use `numbers` or `letters` in order. This setting will set the default selected value in the create form. | +| default_y_axis_labels | "numbers" | "numbers" | Label style for the floor plan grid. Can use `numbers` or `letters` in order. This setting will set the default selected value in the create form. | diff --git a/nautobot_floor_plan/app-config-schema.json b/nautobot_floor_plan/app-config-schema.json index 27ba77d..01e42ee 100644 --- a/nautobot_floor_plan/app-config-schema.json +++ b/nautobot_floor_plan/app-config-schema.json @@ -1 +1,17 @@ -true +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/nautobot/nautobot-app-floor-plan/develop/nautobot_floor_plan/app-config-schema.json", + "$comment": "TBD: Update $id, replace `develop` with the future release tag", + "type": "object", + "properties": { + "default_x_axis_labels": { + "type": "string", + "default": "numbers" + }, + "default_y_axis_labels": { + "type": "string", + "default": "numbers" + } + }, + "additionalProperties": false +} diff --git a/nautobot_floor_plan/forms.py b/nautobot_floor_plan/forms.py index 7e2b70b..af98efb 100644 --- a/nautobot_floor_plan/forms.py +++ b/nautobot_floor_plan/forms.py @@ -3,7 +3,6 @@ # pylint: disable=nb-incorrect-base-class """Forms for nautobot_floor_plan.""" -import re from django import forms @@ -124,14 +123,14 @@ def __init__(self, *args, **kwargs): def letter_validator(self, field, value, axis): """Validate that origin uses combination of letters.""" - if not re.search(r"[A-Z]+", str(value)): + if not str(value).isupper(): self.add_error(field, f"{axis} origin should use capital letters.") return False return True def number_validator(self, field, value, axis): """Validate that origin uses combination of numbers.""" - if not re.search(r"\d+", str(value)): + if not str(value).isdigit(): self.add_error(field, f"{axis} origin should use numbers.") return False return True From a75777019b418d732bd1df4dc67614d824615841 Mon Sep 17 00:00:00 2001 From: pszulczewski Date: Fri, 5 Apr 2024 17:58:14 +0200 Subject: [PATCH 16/16] Add app config validation. --- nautobot_floor_plan/__init__.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/nautobot_floor_plan/__init__.py b/nautobot_floor_plan/__init__.py index 1d06f92..8b57732 100644 --- a/nautobot_floor_plan/__init__.py +++ b/nautobot_floor_plan/__init__.py @@ -2,8 +2,12 @@ # Metadata is inherited from Nautobot. If not including Nautobot in the environment, this should be added from importlib import metadata +from django.core.exceptions import ImproperlyConfigured + from nautobot.apps import NautobotAppConfig -from nautobot_floor_plan import choices +from nautobot.apps.config import get_app_settings_or_config + +from nautobot_floor_plan.choices import AxisLabelsChoices __version__ = metadata.version(__name__) @@ -21,10 +25,24 @@ class FloorPlanConfig(NautobotAppConfig): min_version = "2.0.0" max_version = "2.9999" default_settings = { - "default_x_axis_labels": choices.AxisLabelsChoices.NUMBERS, - "default_y_axis_labels": choices.AxisLabelsChoices.NUMBERS, + "default_x_axis_labels": AxisLabelsChoices.NUMBERS, + "default_y_axis_labels": AxisLabelsChoices.NUMBERS, } caching_config = {} + def validate_config_options(self): + """Validates app configuration options.""" + x_axis_labels = get_app_settings_or_config("nautobot_floor_plan", "default_x_axis_labels") + y_axis_labels = get_app_settings_or_config("nautobot_floor_plan", "default_y_axis_labels") + valid_choices = AxisLabelsChoices.values() + if x_axis_labels not in valid_choices or y_axis_labels not in valid_choices: + msg = f"nautobot_floor_plan improperly configured. Valid config options for default_x_axis_labels or default_y_axis_labels are: {', '.join(valid_choices)}, plugin config is: default_x_axis_labels: {x_axis_labels}, default_y_axis_labels: {y_axis_labels}" + raise ImproperlyConfigured(msg) + + def ready(self): + """Callback after app is loaded.""" + super().ready() + self.validate_config_options() + config = FloorPlanConfig # pylint:disable=invalid-name