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

Added option to define step values for grid labeling #115

Merged
merged 10 commits into from
Sep 12, 2024
1 change: 1 addition & 0 deletions changes/8.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added grid step option to FloorPlan.
Binary file modified docs/images/add-floor-plan-form.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions docs/user/app_getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ To install the App, please follow the instructions detailed in the [Installation

As a first step you will want to define which Status(es) can be applied to individual tiles in a floor plan. This can be done by navigating to "Organization > Statuses" in the Nautobot UI, and creating or updating the desired Status records to include `nautobot_floor_plan | floor plan tile` as one of the Status's "Content Types".

The app installs with the the following statuses by default. `"Active", "Reserved", "Decommissioning", "Unavailable", "Planned"`

![Status definition](../images/status-definition.png)

## What are the next steps?
Expand All @@ -24,6 +26,12 @@ Clicking this button will bring you to a standard Nautobot create/edit form, in

The "X size" and "Y size" parameters define the number of Tiles in the Floor Plan, and the "Tile width" and "Tile depth" parameters define the relative proportions of each Tile when rendered in the Nautobot UI. You can leave the tile parameters as defaults for a square grid, or set them as desired for a off-square rectangular grid.

The "X Axis Labels" and "Y Axis Labels" parameters can be used to represent "Numbers" or "Letters" for grid labeling. The default setting is "Numbers".

The "X Axis Seed" and "Y Axis Seed" parameters allow you to define the starting location for a grid label. The default setting is "1".

The "X Axis Step" and "Y Axis Step" parameters allow you to choose a positive or negative integer step value that are used to skip numbers or letters for grid labeling. The default setting is "1".

After clicking "Create", you will be presented with a new floor plan render:

![Empty floor plan](../images/floor-plan-empty.png)
Expand Down
3 changes: 3 additions & 0 deletions docs/user/app_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ Included is a non-exhaustive list of capabilites beyond a standard MVC (model vi
- Provides the ability to span multiple adjacent tiles by a single rack.
- Provides the ability to place racks in a group that spans multiple tiles.
- Provides custom layout size in any rectangular shape using X & Y axis.
- Provides the abililty to choose Numbers or Letters for grid labels.
- Provides the ability for a user to define a specific number or letter as a starting point for grid labels.
- Provides the ability for a user to define a positive or negative integer to allow for the skipping of letters or numbers for grid labels.
- Provides the ability to save the generated SVG from a click of a "Save SVG" link.

## Nautobot Features Used
Expand Down
14 changes: 14 additions & 0 deletions nautobot_floor_plan/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ class FloorPlanForm(NautobotModelForm):
help_text="The first value to begin Y Axis at.",
required=False,
)
x_axis_step = forms.IntegerField(
label="X Axis Step",
help_text="A positive or negative integer, excluding zero",
required=False,
validators=[utils.validate_not_zero],
djhoward12 marked this conversation as resolved.
Show resolved Hide resolved
)
djhoward12 marked this conversation as resolved.
Show resolved Hide resolved
y_axis_step = forms.IntegerField(
label="Y Axis Step",
help_text="A positive or negative integer, excluding zero",
required=False,
validators=[utils.validate_not_zero],
)
djhoward12 marked this conversation as resolved.
Show resolved Hide resolved

class Meta:
"""Meta attributes."""
Expand All @@ -50,8 +62,10 @@ class Meta:
"tile_depth",
"x_axis_labels",
"x_origin_seed",
"x_axis_step",
"y_axis_labels",
"y_origin_seed",
"y_axis_step",
"tags",
]

Expand Down
23 changes: 23 additions & 0 deletions nautobot_floor_plan/migrations/0008_add_axis_step.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.25 on 2024-08-12 18:59

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("nautobot_floor_plan", "0007_add_axis_origin_seed"),
]

operations = [
migrations.AddField(
model_name="floorplan",
name="x_axis_step",
field=models.IntegerField(default=1),
),
migrations.AddField(
model_name="floorplan",
name="y_axis_step",
field=models.IntegerField(default=1),
),
]
20 changes: 18 additions & 2 deletions nautobot_floor_plan/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from nautobot_floor_plan.choices import RackOrientationChoices, AxisLabelsChoices, AllocationTypeChoices
from nautobot_floor_plan.svg import FloorPlanSVG

from nautobot_floor_plan.utils import validate_not_zero


logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -65,8 +67,20 @@ class FloorPlan(PrimaryModel):
default=AxisLabelsChoices.NUMBERS,
help_text="Grid labels of Y axis (vertical).",
)
x_origin_seed = models.PositiveSmallIntegerField(validators=[MinValueValidator(0)], default=1)
y_origin_seed = models.PositiveSmallIntegerField(validators=[MinValueValidator(0)], default=1)
x_origin_seed = models.PositiveSmallIntegerField(
validators=[MinValueValidator(0)], default=1, help_text="User defined starting value for grid labeling"
)
y_origin_seed = models.PositiveSmallIntegerField(
validators=[MinValueValidator(0)], default=1, help_text="User defined starting value for grid labeling"
)
x_axis_step = models.IntegerField(
default=1,
help_text="Positive or negative integer that will be used to step labeling.",
)
y_axis_step = models.IntegerField(
default=1,
help_text="Positive or negative integer that will be used to step labeling.",
)

class Meta:
"""Metaclass attributes."""
Expand All @@ -83,6 +97,8 @@ def get_svg(self, *, user, base_url):

def save(self, *args, **kwargs):
"""Override save in order to update any existing tiles."""
validate_not_zero(self.x_axis_step)
djhoward12 marked this conversation as resolved.
Show resolved Hide resolved
validate_not_zero(self.y_axis_step)
if self.present_in_database:
# Get origin_seed pre/post values
initial_instance = self.__class__.objects.get(pk=self.pk)
Expand Down
24 changes: 22 additions & 2 deletions nautobot_floor_plan/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,18 @@ def _setup_drawing(self, width, depth):

return drawing

def _label_text(self, label_text_out, step, seed, label_text_in):
"""Change label based off defined increment or decrement step."""
if label_text_out == seed:
return label_text_out
label_text_out = label_text_in + step
return label_text_out

def _draw_grid(self, drawing):
"""Render the grid underlying all tiles."""
# Set inital values for x and y axis label location
x_label_text = 0
y_label_text = 0
# Vertical lines
for x in range(0, self.floor_plan.x_size + 1):
drawing.add(
Expand All @@ -117,7 +127,12 @@ def _draw_grid(self, drawing):
)
# Axis labels
for x in range(self.floor_plan.x_origin_seed, self.floor_plan.x_size + self.floor_plan.x_origin_seed):
label = grid_number_to_letter(x) if self.floor_plan.x_axis_labels == AxisLabelsChoices.LETTERS else str(x)
x_label_text = self._label_text(x, self.floor_plan.x_axis_step, self.floor_plan.x_origin_seed, x_label_text)
label = (
grid_number_to_letter(x_label_text)
if self.floor_plan.x_axis_labels == AxisLabelsChoices.LETTERS
else str(x_label_text)
)
drawing.add(
drawing.text(
label,
Expand All @@ -129,7 +144,12 @@ def _draw_grid(self, drawing):
)
)
for y in range(self.floor_plan.y_origin_seed, self.floor_plan.y_size + self.floor_plan.y_origin_seed):
label = grid_number_to_letter(y) if self.floor_plan.y_axis_labels == AxisLabelsChoices.LETTERS else str(y)
y_label_text = self._label_text(y, self.floor_plan.y_axis_step, self.floor_plan.y_origin_seed, y_label_text)
label = (
grid_number_to_letter(y_label_text)
if self.floor_plan.y_axis_labels == AxisLabelsChoices.LETTERS
else str(y_label_text)
)
drawing.add(
drawing.text(
label,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
<td>X Axis Seed</td>
<td>{{ object|seed_conversion:'x' }}</td>
</tr>
<tr>
<td>X Axis Step </td>
<td>{{ object.x_axis_step }}</td>
</tr>
<tr>
<td>Y Axis Labels</td>
<td>{{ object.y_axis_labels }}</td>
Expand All @@ -50,6 +54,10 @@
<td>Y Axis Seed</td>
<td>{{ object|seed_conversion:'y' }}</td>
</tr>
<tr>
<td>Y Axis Step </td>
<td>{{ object.y_axis_step }}</td>
</tr>
<tr>
<td>Tiles</td>
<td><a href="{% url 'plugins:nautobot_floor_plan:floorplantile_list' %}?floor_plan={{ object.pk }}">{{ object.tiles.count }}</a></td>
Expand Down
4 changes: 4 additions & 0 deletions nautobot_floor_plan/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def test_valid_extra_inputs(self):
"tile_width": 2,
"x_axis_labels": choices.AxisLabelsChoices.NUMBERS,
"y_axis_labels": choices.AxisLabelsChoices.NUMBERS,
"x_axis_step": 2,
"y_axis_step": 1,
"tags": [tag],
}
)
Expand All @@ -63,6 +65,8 @@ def test_valid_extra_inputs(self):
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(floor_plan.x_axis_step, 2)
self.assertEqual(floor_plan.y_axis_step, 1)
self.assertEqual(list(floor_plan.tags.all()), [tag])

def test_invalid_required_fields(self):
Expand Down
11 changes: 11 additions & 0 deletions nautobot_floor_plan/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ def test_create_floor_plan_valid(self):
floor_plan_minimal.validated_save()
floor_plan_huge = models.FloorPlan(location=self.floors[1], x_size=100, y_size=100)
floor_plan_huge.validated_save()
floor_plan_pos_neg_step = models.FloorPlan(
location=self.floors[2], x_size=20, y_size=20, x_axis_step=-1, y_axis_step=2
)
floor_plan_pos_neg_step.validated_save()

def test_create_floor_plan_invalid_no_location(self):
"""Can't create a FloorPlan with no Location."""
Expand Down Expand Up @@ -103,6 +107,13 @@ def test_origin_seed_x_increase_y_decrease(self):
self.assertEqual(floor_plan.tiles.get(id=ids[3]).x_origin, 5)
self.assertEqual(floor_plan.tiles.get(id=ids[3]).y_origin, 4)

def test_create_floor_plan_invalid_step(self):
"""A FloorPlan must not use a step value of zero."""
with self.assertRaises(ValidationError):
models.FloorPlan(
location=self.floors[1], x_size=100, y_size=100, x_axis_step=0, y_axis_step=2
).validated_save()


class TestFloorPlanTile(TestCase):
"""Test FloorPlanTile model."""
Expand Down
8 changes: 8 additions & 0 deletions nautobot_floor_plan/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Utilities module."""

from django.core.exceptions import ValidationError


def grid_number_to_letter(number):
"""Returns letter for number [1 - 26] --> [A - Z], [27 - 52] --> [AA - AZ]."""
Expand All @@ -20,3 +22,9 @@ def grid_letter_to_number(letter):
if letter[:-1]:
return 26 * (grid_letter_to_number(letter[:-1])) + number
return number


def validate_not_zero(value):
Copy link
Contributor

Choose a reason for hiding this comment

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

Not blocking, but to get around the validate_not_zero.message and follow established Django Pattern, you can change this to a class. In case you need this in the future.

Suggested change
def validate_not_zero(value):
from django.core.validators import BaseValidator
class NonZeroValidator(BaseValidator):
"""
Ensure that the field's value is not zero.
"""
message = "Must be a positive or negative Integer not equal to zero."
code = "zero_not_allowed"
def __call__(self, value):
if value == 0:
raise ValidationError(
self.message,
code=self.code,
)

"""Prevent the usage of 0 as a value in the step form field or model attribute."""
if value == 0:
raise ValidationError(("Value must be a positive or negative Integer not equal to zero"))