Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add usage of letters for rendering FP SVG. #79

Merged
merged 16 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/79.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added an option to change the grid column/row labels to use letters rather than numbers.
16 changes: 16 additions & 0 deletions docs/admin/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 labels like a chessboard (as seen in this example)
PLUGINS_CONFIG = {
"nautobot_floor_plan": {
"default_x_axis_labels": "letters",
pszulczewski marked this conversation as resolved.
Show resolved Hide resolved
}
}
```

Once the Nautobot configuration is updated, run the Post Upgrade command (`nautobot-server post_upgrade`) to run migrations and clear any cache:
Expand All @@ -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 |
|--------------------|-----------|----------|------------------------------------------------------------------------------------------------------------------------------------------------|
| 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. |
pszulczewski marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 5 additions & 1 deletion nautobot_floor_plan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from importlib import metadata

from nautobot.apps import NautobotAppConfig
from nautobot_floor_plan import choices

__version__ = metadata.version(__name__)

Expand All @@ -19,7 +20,10 @@ class FloorPlanConfig(NautobotAppConfig):
required_settings = []
min_version = "2.0.0"
max_version = "2.9999"
default_settings = {}
default_settings = {
"default_x_axis_labels": choices.AxisLabelsChoices.NUMBERS,
"default_y_axis_labels": choices.AxisLabelsChoices.NUMBERS,
}
caching_config = {}


Expand Down
12 changes: 12 additions & 0 deletions nautobot_floor_plan/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
)
69 changes: 68 additions & 1 deletion nautobot_floor_plan/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# pylint: disable=nb-incorrect-base-class

"""Forms for nautobot_floor_plan."""
import re

from django import forms

from nautobot.dcim.models import Location, Rack
Expand All @@ -14,9 +16,11 @@
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
TagFilterField,
add_blank_choice,
)
from nautobot.apps.config import get_app_settings_or_config

from nautobot_floor_plan import models
from nautobot_floor_plan import models, choices, utils


class FloorPlanForm(NautobotModelForm):
Expand All @@ -34,9 +38,18 @@ class Meta:
"y_size",
"tile_width",
"tile_depth",
"x_axis_labels",
"y_axis_labels",
"tags",
]

def __init__(self, *args, **kwargs):
"""Overwrite the constructor to set initial values for select widget."""
super().__init__(*args, **kwargs)
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")
glennmatthews marked this conversation as resolved.
Show resolved Hide resolved


class FloorPlanBulkEditForm(TagsBulkEditFormMixin, NautobotBulkEditForm):
"""FloorPlan bulk edit form."""
Expand All @@ -46,6 +59,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."""
Expand All @@ -71,6 +86,8 @@ class FloorPlanTileForm(NautobotModelForm):
rack = DynamicModelChoiceField(
queryset=Rack.objects.all(), required=False, query_params={"nautobot_floor_plan_floor_plan": "$floor_plan"}
)
x_origin = forms.CharField()
y_origin = forms.CharField()

class Meta:
"""Meta attributes."""
Expand All @@ -87,3 +104,53 @@ class Meta:
"rack_orientation",
"tags",
]

def __init__(self, *args, **kwargs):
"""Overwrite the constructor to define grid numbering style."""
super().__init__(*args, **kwargs)
self.x_letters = False
self.y_letters = False

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

if self.instance.x_origin or self.instance.y_origin:
if self.x_letters:
self.initial["x_origin"] = utils.grid_number_to_letter(self.instance.x_origin)
if self.y_letters:
self.initial["y_origin"] = utils.grid_number_to_letter(self.instance.y_origin)

def letter_validator(self, field, value, axis):
"""Validate that origin uses combination of letters."""
if not re.search(r"[A-Z]+", str(value)):
pszulczewski marked this conversation as resolved.
Show resolved Hide resolved
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)):
self.add_error(field, f"{axis} origin should use numbers.")
return False
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
Comment on lines +142 to +143
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on this? I'd think that if the validator fails we should fail model clean?

Copy link
Contributor Author

@pszulczewski pszulczewski Apr 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking to raise ValidationError in here and I was expecting that it will stop here, but what happens is that ValidationError exception is raised silently and it still goes to the model .clean() method, where it causes exception on the > operator TypeError: '>' not supported between instances of 'str' and 'int', what results with an exception page in the UI.

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."""
return self._clean_origin("x_origin", "X")

def clean_y_origin(self):
"""Validate input and convert y_origin to an integer."""
return self._clean_origin("y_origin", "Y")
22 changes: 22 additions & 0 deletions nautobot_floor_plan/migrations/0004_auto_20240321_1438.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
14 changes: 13 additions & 1 deletion nautobot_floor_plan/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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 labels of X axis (horizontal).",
)
y_axis_labels = models.CharField(
max_length=10,
choices=AxisLabelsChoices,
default=AxisLabelsChoices.NUMBERS,
help_text="Grid labels of Y axis (vertical).",
)

class Meta:
"""Metaclass attributes."""
Expand Down
15 changes: 10 additions & 5 deletions nautobot_floor_plan/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

from nautobot.core.templatetags.helpers import fgcolor

from nautobot_floor_plan.choices import RackOrientationChoices
from nautobot_floor_plan.choices import RackOrientationChoices, AxisLabelsChoices
from nautobot_floor_plan.utils import grid_number_to_letter


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -109,9 +110,10 @@ def _draw_grid(self, drawing):
)
# Axis labels
for x in range(1, self.floor_plan.x_size + 1):
label = grid_number_to_letter(x) if self.floor_plan.x_axis_labels == AxisLabelsChoices.LETTERS else str(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,
Expand All @@ -120,9 +122,10 @@ def _draw_grid(self, drawing):
)
)
for y in range(1, self.floor_plan.y_size + 1):
label = grid_number_to_letter(y) if self.floor_plan.y_axis_labels == AxisLabelsChoices.LETTERS else str(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,
Expand All @@ -132,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": 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,
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@
<td>Tile Depth (Relative Units)</td>
<td>{{ object.tile_depth }}</td>
</tr>
<tr>
<td>X Axis Labels</td>
<td>{{ object.x_axis_labels }}</td>
</tr>
<tr>
<td>Y Axis Labels</td>
<td>{{ object.y_axis_labels }}</td>
</tr>
</table>
</div>
{% endblock content_left_page %}
Expand Down
1 change: 1 addition & 0 deletions nautobot_floor_plan/tests/test_api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading