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

Djhoward12 naaps 131 form grid conversion #134

Merged
1 change: 1 addition & 0 deletions changes/131.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed wrap-around for letters going negative from A to ZZZ and updated display of labels in form.
1 change: 1 addition & 0 deletions changes/134.housekeeping
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Refactored svg.py to reduce redundant code and local variables.
1 change: 1 addition & 0 deletions changes/136.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed grid label spacing on Y-Axis by checking the length of all labels to determine correct offset.
60 changes: 46 additions & 14 deletions nautobot_floor_plan/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def _clean_origin_seed(self, field_name, axis):
return 0
return utils.grid_letter_to_number(value)

if not str(value).isdigit():
if not str(value).replace("-", "").isnumeric():
self.add_error(field_name, f"{axis} origin start should use numbers.")
return 0
return int(value)
Expand Down Expand Up @@ -196,10 +196,25 @@ def __init__(self, *args, **kwargs):
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)
self.initial["x_origin"] = utils.axis_init_label_conversion(
fp_obj.x_origin_seed,
utils.grid_number_to_letter(self.instance.x_origin) if self.x_letters else self.initial.get("x_origin"),
fp_obj.x_axis_step,
self.x_letters,
)
self.initial["y_origin"] = utils.axis_init_label_conversion(
fp_obj.y_origin_seed,
utils.grid_number_to_letter(self.instance.y_origin) if self.y_letters else self.initial.get("y_origin"),
fp_obj.y_axis_step,
self.y_letters,
)
elif self.initial.get("x_origin") and self.initial.get("y_origin"):
self.initial["x_origin"] = utils.axis_init_label_conversion(
fp_obj.x_origin_seed, self.initial.get("x_origin"), fp_obj.x_axis_step, self.x_letters
)
self.initial["y_origin"] = utils.axis_init_label_conversion(
fp_obj.y_origin_seed, self.initial.get("y_origin"), fp_obj.y_axis_step, self.y_letters
)

def letter_validator(self, field, value, axis):
"""Validate that origin uses combination of letters."""
Expand All @@ -209,22 +224,39 @@ def letter_validator(self, field, value, axis):
return True

def number_validator(self, field, value, axis):
"""Validate that origin uses combination of numbers."""
if not str(value).isdigit():
"""Validate that origin uses combination of positive or negative numbers."""
if not str(value).replace("-", "").isnumeric():
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."""
# Retrieve floor plan object if available
fp_id = self.initial.get("floor_plan") or self.data.get("floor_plan")
if not fp_id:
return 0

fp_obj = self.fields["floor_plan"].queryset.get(id=fp_id)
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)

# Determine if letters are being used for x or y axis labels
using_letters = (field_name == "x_origin" and self.x_letters) or (field_name == "y_origin" and self.y_letters)

# Perform validation based on the type (letters or numbers)
validator = self.letter_validator if using_letters else self.number_validator
if not validator(field_name, value, axis):
return 0 # Required to pass model clean() method

# Select the appropriate axis seed and step
if field_name == "x_origin":
origin_seed, step, use_letters = fp_obj.x_origin_seed, fp_obj.x_axis_step, self.x_letters
else:
origin_seed, step, use_letters = fp_obj.y_origin_seed, fp_obj.y_axis_step, self.y_letters

# Convert and return the label position using the specified conversion function
cleaned_value = utils.axis_clean_label_conversion(origin_seed, value, step, use_letters)
return int(cleaned_value) if not using_letters else cleaned_value

def clean_x_origin(self):
"""Validate input and convert x_origin to an integer."""
Expand Down
140 changes: 71 additions & 69 deletions nautobot_floor_plan/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,77 @@ def _setup_drawing(self, width, depth):

return drawing

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

# Handle negative values and wrapping
if is_letters and label_text_out <= 0:
label_text_out = 18278 if label_text_in == 0 else 18278 + (label_text_in + floor_plan["step"])

return label_text_out

def _draw_tile_link(self, drawing, axis, x_letters, y_letters):
query_params = urlencode(
{
"floor_plan": self.floor_plan.pk,
"x_origin": grid_number_to_letter(axis["x"]) if x_letters else axis["x"],
"y_origin": grid_number_to_letter(axis["y"]) if y_letters else axis["y"],
"return_url": self.return_url,
}
)
add_url = f"{self.add_url}?{query_params}"
add_link = drawing.add(drawing.a(href=add_url, target="_top"))

add_link.add(
drawing.rect(
(
(axis["x"] - self.floor_plan.x_origin_seed + 0.5) * self.GRID_SIZE_X
+ self.GRID_OFFSET
- (self.TEXT_LINE_HEIGHT / 2),
(axis["y"] - self.floor_plan.y_origin_seed + 0.5) * self.GRID_SIZE_Y
+ self.GRID_OFFSET
- (self.TEXT_LINE_HEIGHT / 2),
),
(self.TEXT_LINE_HEIGHT, self.TEXT_LINE_HEIGHT),
class_="add-tile-button",
rx=self.CORNER_RADIUS,
)
)
add_link.add(
drawing.text(
"+",
insert=(
(axis["x"] - self.floor_plan.x_origin_seed + 0.5) * self.GRID_SIZE_X + self.GRID_OFFSET,
(axis["y"] - self.floor_plan.y_origin_seed + 0.5) * self.GRID_SIZE_Y + self.GRID_OFFSET,
),
class_="button-text",
)
)

def _draw_grid(self, drawing):
"""Render the grid underlying all tiles."""
# Set inital values for x and y axis label location
x_letters = self.floor_plan.x_axis_labels == AxisLabelsChoices.LETTERS
y_letters = self.floor_plan.y_axis_labels == AxisLabelsChoices.LETTERS
x_floor_plan = {"seed": self.floor_plan.x_origin_seed, "step": self.floor_plan.x_axis_step}
y_floor_plan = {"seed": self.floor_plan.y_origin_seed, "step": self.floor_plan.y_axis_step}
# Initial states for labels
x_label_text = 0
y_label_text = 0
# Setting intial value for y axis label text to 0
y_label_text_offset = 0
# Vertical lines
max_y_length = max(
len(str(self._label_text(y, y_floor_plan, 0, y_letters)))
for y in range(self.floor_plan.y_origin_seed, self.floor_plan.y_size + self.floor_plan.y_origin_seed)
)
y_label_text_offset = (
self.Y_LABEL_TEXT_OFFSET - (6 - len(str(self.floor_plan.y_origin_seed))) if max_y_length > 1 else 0
)
if max_y_length >= 4:
y_label_text_offset = self.Y_LABEL_TEXT_OFFSET + 4

# Draw grid lines
for x in range(0, self.floor_plan.x_size + 1):
drawing.add(
drawing.line(
Expand All @@ -114,7 +170,6 @@ def _draw_grid(self, drawing):
class_="grid",
)
)
# Horizontal lines
for y in range(0, self.floor_plan.y_size + 1):
drawing.add(
drawing.line(
Expand All @@ -126,16 +181,11 @@ def _draw_grid(self, drawing):
class_="grid",
)
)
# Axis labels

# Draw axis labels and links
for x in range(self.floor_plan.x_origin_seed, self.floor_plan.x_size + self.floor_plan.x_origin_seed):
x_label_text = self._label_text(x, self.floor_plan.x_axis_step, self.floor_plan.x_origin_seed, x_label_text)
if self.floor_plan.x_axis_labels == AxisLabelsChoices.LETTERS and x_label_text == 0:
x_label_text = 26
label = (
grid_number_to_letter(x_label_text)
if self.floor_plan.x_axis_labels == AxisLabelsChoices.LETTERS
else str(x_label_text)
)
x_label_text = self._label_text(x, x_floor_plan, x_label_text, x_letters)
label = grid_number_to_letter(x_label_text) if x_letters else str(x_label_text)
drawing.add(
drawing.text(
label,
Expand All @@ -146,20 +196,10 @@ def _draw_grid(self, drawing):
class_="grid-label",
)
)

for y in range(self.floor_plan.y_origin_seed, self.floor_plan.y_size + self.floor_plan.y_origin_seed):
y_label_text = self._label_text(y, self.floor_plan.y_axis_step, self.floor_plan.y_origin_seed, y_label_text)
if self.floor_plan.y_axis_labels == AxisLabelsChoices.LETTERS and y_label_text == 0:
y_label_text = 26
label = (
grid_number_to_letter(y_label_text)
if self.floor_plan.y_axis_labels == AxisLabelsChoices.LETTERS
else str(y_label_text)
)
# Adjust the starting position of the y_axis_label text if the length of the inital SEED value is greater than 1
if len(str(self.floor_plan.y_origin_seed)) > 1:
y_label_text_offset = self.Y_LABEL_TEXT_OFFSET - (6 - len(str(self.floor_plan.y_origin_seed)))
if len(str(self.floor_plan.y_origin_seed)) > 4:
y_label_text_offset = self.Y_LABEL_TEXT_OFFSET + 4
y_label_text = self._label_text(y, y_floor_plan, y_label_text, y_letters)
label = grid_number_to_letter(y_label_text) if y_letters else str(y_label_text)
drawing.add(
drawing.text(
label,
Expand All @@ -171,48 +211,10 @@ 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(self.floor_plan.y_origin_seed, self.floor_plan.y_size + self.floor_plan.y_origin_seed):
for x in range(self.floor_plan.x_origin_seed, self.floor_plan.x_size + self.floor_plan.x_origin_seed):
query_params = urlencode(
{
"floor_plan": self.floor_plan.pk,
"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,
}
)
add_url = f"{self.add_url}?{query_params}"
add_link = drawing.add(drawing.a(href=add_url, target="_top"))
# "add" button
add_link.add(
drawing.rect(
(
(x - self.floor_plan.x_origin_seed + 0.5) * self.GRID_SIZE_X
+ self.GRID_OFFSET
- (self.TEXT_LINE_HEIGHT / 2),
(y - self.floor_plan.y_origin_seed + 0.5) * self.GRID_SIZE_Y
+ self.GRID_OFFSET
- (self.TEXT_LINE_HEIGHT / 2),
),
(self.TEXT_LINE_HEIGHT, self.TEXT_LINE_HEIGHT),
class_="add-tile-button",
rx=self.CORNER_RADIUS,
)
)
# "+" inside the add button
add_link.add(
drawing.text(
"+",
insert=(
(x - self.floor_plan.x_origin_seed + 0.5) * self.GRID_SIZE_X + self.GRID_OFFSET,
(y - self.floor_plan.y_origin_seed + 0.5) * self.GRID_SIZE_Y + self.GRID_OFFSET,
),
class_="button-text",
)
)
axis = {"x": x, "y": y}
self._draw_tile_link(drawing, axis, x_letters, y_letters)

def _draw_edit_delete_button(self, drawing, tile, button_offset, grid_offset):
if tile.allocation_type == AllocationTypeChoices.RACK:
Expand Down
50 changes: 50 additions & 0 deletions nautobot_floor_plan/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,56 @@ def grid_letter_to_number(letter):
return number


def axis_init_label_conversion(axis_origin, axis_location, step, letters):
djhoward12 marked this conversation as resolved.
Show resolved Hide resolved
"""Returns the correct label position, converting to letters if `letters` is True."""
if letters:
axis_location = grid_letter_to_number(axis_location)
# Calculate the converted location based on origin, step, and location
converted_location = axis_origin + (int(axis_location) - int(axis_origin)) * step
# Check if we need wrap around due to letters being chosen
if letters:
# Set wrap around value of ZZZ
total_cells = 18278
# Adjust for wrap-around when working with letters A or ZZZ
if converted_location < 1:
converted_location = total_cells + converted_location
elif converted_location > total_cells:
converted_location -= total_cells
result_label = grid_number_to_letter(converted_location)
return result_label
return converted_location


def axis_clean_label_conversion(axis_origin, axis_label, step, letters):
"""Returns the correct database label position."""
total_cells = 18278
# Convert letters to numbers if needed
if letters:
axis_label = grid_letter_to_number(axis_label)

# Reverse the init conversion logic to determine the numeric position
position_difference = int(axis_label) - int(axis_origin)

if step < 0:
# Adjust for wrap-around when working with letters A or ZZZ
if int(axis_label) > int(axis_origin):
position_difference -= total_cells
else:
if int(axis_label) < int(axis_origin):
position_difference += total_cells

# Calculate the original location using the step
original_location = axis_origin + (position_difference // step)

# Ensure original location stays within bounds for letters
if letters:
if original_location < 1:
original_location += total_cells
elif original_location > total_cells:
original_location -= total_cells
return str(original_location)


def validate_not_zero(value):
"""Prevent the usage of 0 as a value in the step form field or model attribute."""
if value == 0:
Expand Down