We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
"service" does not preserve the order of ports when editing
### NetBox version

v3.6.5

### Python version

3.8

### Steps to Reproduce

1. Create a "service", and enter the ports as "9093,9095,9998-9999"
2. View the list of services
3. Edit the service (i.e. click the pencil icon at the end of the service row)

### Expected Behavior

Either the ports to remain in the order originally entered, or to be sorted. (I note that the data type in the underlying Postgres column is `integer[]` which is an ordered list)

### Observed Behavior

When viewing the table of services (`/ipam/services/`), the ports are shown in order:

<img width="304" alt="image" src="https://github.com/netbox-community/netbox/assets/44789/632b5313-7241-45d3-8649-b16fb9c4b6f0">

It also shows the same when viewing the details of an individual service (e.g. `/ipam/services/2/`)

However, when editing the service (`/ipam/services/2/edit/`), the ports are in a randomized order:

<img width="681" alt="image" src="https://github.com/netbox-community/netbox/assets/44789/494f89cc-80b5-4b48-a27f-498992c159e3">

This matches what's in the database, which in the same randomized order:

```
netbox=# select ports from ipam_service where id=2
         ports
-----------------------
 {9999,9093,9998,9095}
(1 row)
```


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `netbox/utilities/forms/utils.py`
Content:
```
1 import re
2 
3 from django import forms
4 from django.forms.models import fields_for_model
5 
6 from utilities.choices import unpack_grouped_choices
7 from utilities.querysets import RestrictedQuerySet
8 from .constants import *
9 
10 __all__ = (
11     'add_blank_choice',
12     'expand_alphanumeric_pattern',
13     'expand_ipaddress_pattern',
14     'form_from_model',
15     'get_field_value',
16     'get_selected_values',
17     'parse_alphanumeric_range',
18     'parse_numeric_range',
19     'restrict_form_fields',
20     'parse_csv',
21     'validate_csv',
22 )
23 
24 
25 def parse_numeric_range(string, base=10):
26     """
27     Expand a numeric range (continuous or not) into a decimal or
28     hexadecimal list, as specified by the base parameter
29       '0-3,5' => [0, 1, 2, 3, 5]
30       '2,8-b,d,f' => [2, 8, 9, a, b, d, f]
31     """
32     values = list()
33     for dash_range in string.split(','):
34         try:
35             begin, end = dash_range.split('-')
36         except ValueError:
37             begin, end = dash_range, dash_range
38         try:
39             begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1
40         except ValueError:
41             raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
42         values.extend(range(begin, end))
43     return list(set(values))
44 
45 
46 def parse_alphanumeric_range(string):
47     """
48     Expand an alphanumeric range (continuous or not) into a list.
49     'a-d,f' => [a, b, c, d, f]
50     '0-3,a-d' => [0, 1, 2, 3, a, b, c, d]
51     """
52     values = []
53     for dash_range in string.split(','):
54         try:
55             begin, end = dash_range.split('-')
56             vals = begin + end
57             # Break out of loop if there's an invalid pattern to return an error
58             if (not (vals.isdigit() or vals.isalpha())) or (vals.isalpha() and not (vals.isupper() or vals.islower())):
59                 return []
60         except ValueError:
61             begin, end = dash_range, dash_range
62         if begin.isdigit() and end.isdigit():
63             if int(begin) >= int(end):
64                 raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
65 
66             for n in list(range(int(begin), int(end) + 1)):
67                 values.append(n)
68         else:
69             # Value-based
70             if begin == end:
71                 values.append(begin)
72             # Range-based
73             else:
74                 # Not a valid range (more than a single character)
75                 if not len(begin) == len(end) == 1:
76                     raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
77 
78                 if ord(begin) >= ord(end):
79                     raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
80 
81                 for n in list(range(ord(begin), ord(end) + 1)):
82                     values.append(chr(n))
83     return values
84 
85 
86 def expand_alphanumeric_pattern(string):
87     """
88     Expand an alphabetic pattern into a list of strings.
89     """
90     lead, pattern, remnant = re.split(ALPHANUMERIC_EXPANSION_PATTERN, string, maxsplit=1)
91     parsed_range = parse_alphanumeric_range(pattern)
92     for i in parsed_range:
93         if re.search(ALPHANUMERIC_EXPANSION_PATTERN, remnant):
94             for string in expand_alphanumeric_pattern(remnant):
95                 yield "{}{}{}".format(lead, i, string)
96         else:
97             yield "{}{}{}".format(lead, i, remnant)
98 
99 
100 def expand_ipaddress_pattern(string, family):
101     """
102     Expand an IP address pattern into a list of strings. Examples:
103       '192.0.2.[1,2,100-250]/24' => ['192.0.2.1/24', '192.0.2.2/24', '192.0.2.100/24' ... '192.0.2.250/24']
104       '2001:db8:0:[0,fd-ff]::/64' => ['2001:db8:0:0::/64', '2001:db8:0:fd::/64', ... '2001:db8:0:ff::/64']
105     """
106     if family not in [4, 6]:
107         raise Exception("Invalid IP address family: {}".format(family))
108     if family == 4:
109         regex = IP4_EXPANSION_PATTERN
110         base = 10
111     else:
112         regex = IP6_EXPANSION_PATTERN
113         base = 16
114     lead, pattern, remnant = re.split(regex, string, maxsplit=1)
115     parsed_range = parse_numeric_range(pattern, base)
116     for i in parsed_range:
117         if re.search(regex, remnant):
118             for string in expand_ipaddress_pattern(remnant, family):
119                 yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), string])
120         else:
121             yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), remnant])
122 
123 
124 def get_field_value(form, field_name):
125     """
126     Return the current bound or initial value associated with a form field, prior to calling
127     clean() for the form.
128     """
129     field = form.fields[field_name]
130 
131     if form.is_bound:
132         if data := form.data.get(field_name):
133             if field.valid_value(data):
134                 return data
135 
136     return form.get_initial_for_field(field, field_name)
137 
138 
139 def get_selected_values(form, field_name):
140     """
141     Return the list of selected human-friendly values for a form field
142     """
143     if not hasattr(form, 'cleaned_data'):
144         form.is_valid()
145     filter_data = form.cleaned_data.get(field_name)
146     field = form.fields[field_name]
147 
148     # Non-selection field
149     if not hasattr(field, 'choices'):
150         return [str(filter_data)]
151 
152     # Model choice field
153     if type(field.choices) is forms.models.ModelChoiceIterator:
154         # If this is a single-choice field, wrap its value in a list
155         if not hasattr(filter_data, '__iter__'):
156             values = [filter_data]
157         else:
158             values = filter_data
159 
160     else:
161         # Static selection field
162         choices = unpack_grouped_choices(field.choices)
163         if type(filter_data) not in (list, tuple):
164             filter_data = [filter_data]  # Ensure filter data is iterable
165         values = [
166             label for value, label in choices if str(value) in filter_data or None in filter_data
167         ]
168 
169     # If the field has a `null_option` attribute set and it is selected,
170     # add it to the field's grouped choices.
171     if getattr(field, 'null_option', None) and None in filter_data:
172         values.remove(None)
173         values.insert(0, field.null_option)
174 
175     return values
176 
177 
178 def add_blank_choice(choices):
179     """
180     Add a blank choice to the beginning of a choices list.
181     """
182     return ((None, '---------'),) + tuple(choices)
183 
184 
185 def form_from_model(model, fields):
186     """
187     Return a Form class with the specified fields derived from a model. This is useful when we need a form to be used
188     for creating objects, but want to avoid the model's validation (e.g. for bulk create/edit functions). All fields
189     are marked as not required.
190     """
191     form_fields = fields_for_model(model, fields=fields)
192     for field in form_fields.values():
193         field.required = False
194 
195     return type('FormFromModel', (forms.Form,), form_fields)
196 
197 
198 def restrict_form_fields(form, user, action='view'):
199     """
200     Restrict all form fields which reference a RestrictedQuerySet. This ensures that users see only permitted objects
201     as available choices.
202     """
203     for field in form.fields.values():
204         if hasattr(field, 'queryset') and issubclass(field.queryset.__class__, RestrictedQuerySet):
205             field.queryset = field.queryset.restrict(user, action)
206 
207 
208 def parse_csv(reader):
209     """
210     Parse a csv_reader object into a headers dictionary and a list of records dictionaries. Raise an error
211     if the records are formatted incorrectly. Return headers and records as a tuple.
212     """
213     records = []
214     headers = {}
215 
216     # Consume the first line of CSV data as column headers. Create a dictionary mapping each header to an optional
217     # "to" field specifying how the related object is being referenced. For example, importing a Device might use a
218     # `site.slug` header, to indicate the related site is being referenced by its slug.
219 
220     for header in next(reader):
221         header = header.strip()
222         if '.' in header:
223             field, to_field = header.split('.', 1)
224             if field in headers:
225                 raise forms.ValidationError(f'Duplicate or conflicting column header for "{field}"')
226             headers[field] = to_field
227         else:
228             if header in headers:
229                 raise forms.ValidationError(f'Duplicate or conflicting column header for "{header}"')
230             headers[header] = None
231 
232     # Parse CSV rows into a list of dictionaries mapped from the column headers.
233     for i, row in enumerate(reader, start=1):
234         if len(row) != len(headers):
235             raise forms.ValidationError(
236                 f"Row {i}: Expected {len(headers)} columns but found {len(row)}"
237             )
238         row = [col.strip() for col in row]
239         record = dict(zip(headers.keys(), row))
240         records.append(record)
241 
242     return headers, records
243 
244 
245 def validate_csv(headers, fields, required_fields):
246     """
247     Validate that parsed csv data conforms to the object's available fields. Raise validation errors
248     if parsed csv data contains invalid headers or does not contain required headers.
249     """
250     # Validate provided column headers
251     is_update = False
252     for field, to_field in headers.items():
253         if field == "id":
254             is_update = True
255             continue
256         if field not in fields:
257             raise forms.ValidationError(f'Unexpected column header "{field}" found.')
258         if to_field and not hasattr(fields[field], 'to_field_name'):
259             raise forms.ValidationError(f'Column "{field}" is not a related object; cannot use dots')
260         if to_field and not hasattr(fields[field].queryset.model, to_field):
261             raise forms.ValidationError(f'Invalid related object attribute for column "{field}": {to_field}')
262 
263     # Validate required fields (if not an update)
264     if not is_update:
265         for f in required_fields:
266             if f not in headers:
267                 raise forms.ValidationError(f'Required column header "{f}" not found.')
268
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/netbox/utilities/forms/utils.py b/netbox/utilities/forms/utils.py
--- a/netbox/utilities/forms/utils.py
+++ b/netbox/utilities/forms/utils.py
@@ -40,7 +40,7 @@
         except ValueError:
             raise forms.ValidationError(f'Range "{dash_range}" is invalid.')
         values.extend(range(begin, end))
-    return list(set(values))
+    return sorted(set(values))
 
 
 def parse_alphanumeric_range(string):

{"golden_diff": "diff --git a/netbox/utilities/forms/utils.py b/netbox/utilities/forms/utils.py\n--- a/netbox/utilities/forms/utils.py\n+++ b/netbox/utilities/forms/utils.py\n@@ -40,7 +40,7 @@\n         except ValueError:\n             raise forms.ValidationError(f'Range \"{dash_range}\" is invalid.')\n         values.extend(range(begin, end))\n-    return list(set(values))\n+    return sorted(set(values))\n \n \n def parse_alphanumeric_range(string):\n", "issue": "\"service\" does not preserve the order of ports when editing\n### NetBox version\n\nv3.6.5\n\n### Python version\n\n3.8\n\n### Steps to Reproduce\n\n1. Create a \"service\", and enter the ports as \"9093,9095,9998-9999\"\r\n2. View the list of services\r\n3. Edit the service (i.e. click the pencil icon at the end of the service row)\n\n### Expected Behavior\n\nEither the ports to remain in the order originally entered, or to be sorted. (I note that the data type in the underlying Postgres column is `integer[]` which is an ordered list)\n\n### Observed Behavior\n\nWhen viewing the table of services (`/ipam/services/`), the ports are shown in order:\r\n\r\n<img width=\"304\" alt=\"image\" src=\"https://github.com/netbox-community/netbox/assets/44789/632b5313-7241-45d3-8649-b16fb9c4b6f0\">\r\n\r\nIt also shows the same when viewing the details of an individual service (e.g. `/ipam/services/2/`)\r\n\r\nHowever, when editing the service (`/ipam/services/2/edit/`), the ports are in a randomized order:\r\n\r\n<img width=\"681\" alt=\"image\" src=\"https://github.com/netbox-community/netbox/assets/44789/494f89cc-80b5-4b48-a27f-498992c159e3\">\r\n\r\nThis matches what's in the database, which in the same randomized order:\r\n\r\n```\r\nnetbox=# select ports from ipam_service where id=2\r\n         ports\r\n-----------------------\r\n {9999,9093,9998,9095}\r\n(1 row)\r\n```\r\n\n", "before_files": [{"content": "import re\n\nfrom django import forms\nfrom django.forms.models import fields_for_model\n\nfrom utilities.choices import unpack_grouped_choices\nfrom utilities.querysets import RestrictedQuerySet\nfrom .constants import *\n\n__all__ = (\n    'add_blank_choice',\n    'expand_alphanumeric_pattern',\n    'expand_ipaddress_pattern',\n    'form_from_model',\n    'get_field_value',\n    'get_selected_values',\n    'parse_alphanumeric_range',\n    'parse_numeric_range',\n    'restrict_form_fields',\n    'parse_csv',\n    'validate_csv',\n)\n\n\ndef parse_numeric_range(string, base=10):\n    \"\"\"\n    Expand a numeric range (continuous or not) into a decimal or\n    hexadecimal list, as specified by the base parameter\n      '0-3,5' => [0, 1, 2, 3, 5]\n      '2,8-b,d,f' => [2, 8, 9, a, b, d, f]\n    \"\"\"\n    values = list()\n    for dash_range in string.split(','):\n        try:\n            begin, end = dash_range.split('-')\n        except ValueError:\n            begin, end = dash_range, dash_range\n        try:\n            begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1\n        except ValueError:\n            raise forms.ValidationError(f'Range \"{dash_range}\" is invalid.')\n        values.extend(range(begin, end))\n    return list(set(values))\n\n\ndef parse_alphanumeric_range(string):\n    \"\"\"\n    Expand an alphanumeric range (continuous or not) into a list.\n    'a-d,f' => [a, b, c, d, f]\n    '0-3,a-d' => [0, 1, 2, 3, a, b, c, d]\n    \"\"\"\n    values = []\n    for dash_range in string.split(','):\n        try:\n            begin, end = dash_range.split('-')\n            vals = begin + end\n            # Break out of loop if there's an invalid pattern to return an error\n            if (not (vals.isdigit() or vals.isalpha())) or (vals.isalpha() and not (vals.isupper() or vals.islower())):\n                return []\n        except ValueError:\n            begin, end = dash_range, dash_range\n        if begin.isdigit() and end.isdigit():\n            if int(begin) >= int(end):\n                raise forms.ValidationError(f'Range \"{dash_range}\" is invalid.')\n\n            for n in list(range(int(begin), int(end) + 1)):\n                values.append(n)\n        else:\n            # Value-based\n            if begin == end:\n                values.append(begin)\n            # Range-based\n            else:\n                # Not a valid range (more than a single character)\n                if not len(begin) == len(end) == 1:\n                    raise forms.ValidationError(f'Range \"{dash_range}\" is invalid.')\n\n                if ord(begin) >= ord(end):\n                    raise forms.ValidationError(f'Range \"{dash_range}\" is invalid.')\n\n                for n in list(range(ord(begin), ord(end) + 1)):\n                    values.append(chr(n))\n    return values\n\n\ndef expand_alphanumeric_pattern(string):\n    \"\"\"\n    Expand an alphabetic pattern into a list of strings.\n    \"\"\"\n    lead, pattern, remnant = re.split(ALPHANUMERIC_EXPANSION_PATTERN, string, maxsplit=1)\n    parsed_range = parse_alphanumeric_range(pattern)\n    for i in parsed_range:\n        if re.search(ALPHANUMERIC_EXPANSION_PATTERN, remnant):\n            for string in expand_alphanumeric_pattern(remnant):\n                yield \"{}{}{}\".format(lead, i, string)\n        else:\n            yield \"{}{}{}\".format(lead, i, remnant)\n\n\ndef expand_ipaddress_pattern(string, family):\n    \"\"\"\n    Expand an IP address pattern into a list of strings. Examples:\n      '192.0.2.[1,2,100-250]/24' => ['192.0.2.1/24', '192.0.2.2/24', '192.0.2.100/24' ... '192.0.2.250/24']\n      '2001:db8:0:[0,fd-ff]::/64' => ['2001:db8:0:0::/64', '2001:db8:0:fd::/64', ... '2001:db8:0:ff::/64']\n    \"\"\"\n    if family not in [4, 6]:\n        raise Exception(\"Invalid IP address family: {}\".format(family))\n    if family == 4:\n        regex = IP4_EXPANSION_PATTERN\n        base = 10\n    else:\n        regex = IP6_EXPANSION_PATTERN\n        base = 16\n    lead, pattern, remnant = re.split(regex, string, maxsplit=1)\n    parsed_range = parse_numeric_range(pattern, base)\n    for i in parsed_range:\n        if re.search(regex, remnant):\n            for string in expand_ipaddress_pattern(remnant, family):\n                yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), string])\n        else:\n            yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), remnant])\n\n\ndef get_field_value(form, field_name):\n    \"\"\"\n    Return the current bound or initial value associated with a form field, prior to calling\n    clean() for the form.\n    \"\"\"\n    field = form.fields[field_name]\n\n    if form.is_bound:\n        if data := form.data.get(field_name):\n            if field.valid_value(data):\n                return data\n\n    return form.get_initial_for_field(field, field_name)\n\n\ndef get_selected_values(form, field_name):\n    \"\"\"\n    Return the list of selected human-friendly values for a form field\n    \"\"\"\n    if not hasattr(form, 'cleaned_data'):\n        form.is_valid()\n    filter_data = form.cleaned_data.get(field_name)\n    field = form.fields[field_name]\n\n    # Non-selection field\n    if not hasattr(field, 'choices'):\n        return [str(filter_data)]\n\n    # Model choice field\n    if type(field.choices) is forms.models.ModelChoiceIterator:\n        # If this is a single-choice field, wrap its value in a list\n        if not hasattr(filter_data, '__iter__'):\n            values = [filter_data]\n        else:\n            values = filter_data\n\n    else:\n        # Static selection field\n        choices = unpack_grouped_choices(field.choices)\n        if type(filter_data) not in (list, tuple):\n            filter_data = [filter_data]  # Ensure filter data is iterable\n        values = [\n            label for value, label in choices if str(value) in filter_data or None in filter_data\n        ]\n\n    # If the field has a `null_option` attribute set and it is selected,\n    # add it to the field's grouped choices.\n    if getattr(field, 'null_option', None) and None in filter_data:\n        values.remove(None)\n        values.insert(0, field.null_option)\n\n    return values\n\n\ndef add_blank_choice(choices):\n    \"\"\"\n    Add a blank choice to the beginning of a choices list.\n    \"\"\"\n    return ((None, '---------'),) + tuple(choices)\n\n\ndef form_from_model(model, fields):\n    \"\"\"\n    Return a Form class with the specified fields derived from a model. This is useful when we need a form to be used\n    for creating objects, but want to avoid the model's validation (e.g. for bulk create/edit functions). All fields\n    are marked as not required.\n    \"\"\"\n    form_fields = fields_for_model(model, fields=fields)\n    for field in form_fields.values():\n        field.required = False\n\n    return type('FormFromModel', (forms.Form,), form_fields)\n\n\ndef restrict_form_fields(form, user, action='view'):\n    \"\"\"\n    Restrict all form fields which reference a RestrictedQuerySet. This ensures that users see only permitted objects\n    as available choices.\n    \"\"\"\n    for field in form.fields.values():\n        if hasattr(field, 'queryset') and issubclass(field.queryset.__class__, RestrictedQuerySet):\n            field.queryset = field.queryset.restrict(user, action)\n\n\ndef parse_csv(reader):\n    \"\"\"\n    Parse a csv_reader object into a headers dictionary and a list of records dictionaries. Raise an error\n    if the records are formatted incorrectly. Return headers and records as a tuple.\n    \"\"\"\n    records = []\n    headers = {}\n\n    # Consume the first line of CSV data as column headers. Create a dictionary mapping each header to an optional\n    # \"to\" field specifying how the related object is being referenced. For example, importing a Device might use a\n    # `site.slug` header, to indicate the related site is being referenced by its slug.\n\n    for header in next(reader):\n        header = header.strip()\n        if '.' in header:\n            field, to_field = header.split('.', 1)\n            if field in headers:\n                raise forms.ValidationError(f'Duplicate or conflicting column header for \"{field}\"')\n            headers[field] = to_field\n        else:\n            if header in headers:\n                raise forms.ValidationError(f'Duplicate or conflicting column header for \"{header}\"')\n            headers[header] = None\n\n    # Parse CSV rows into a list of dictionaries mapped from the column headers.\n    for i, row in enumerate(reader, start=1):\n        if len(row) != len(headers):\n            raise forms.ValidationError(\n                f\"Row {i}: Expected {len(headers)} columns but found {len(row)}\"\n            )\n        row = [col.strip() for col in row]\n        record = dict(zip(headers.keys(), row))\n        records.append(record)\n\n    return headers, records\n\n\ndef validate_csv(headers, fields, required_fields):\n    \"\"\"\n    Validate that parsed csv data conforms to the object's available fields. Raise validation errors\n    if parsed csv data contains invalid headers or does not contain required headers.\n    \"\"\"\n    # Validate provided column headers\n    is_update = False\n    for field, to_field in headers.items():\n        if field == \"id\":\n            is_update = True\n            continue\n        if field not in fields:\n            raise forms.ValidationError(f'Unexpected column header \"{field}\" found.')\n        if to_field and not hasattr(fields[field], 'to_field_name'):\n            raise forms.ValidationError(f'Column \"{field}\" is not a related object; cannot use dots')\n        if to_field and not hasattr(fields[field].queryset.model, to_field):\n            raise forms.ValidationError(f'Invalid related object attribute for column \"{field}\": {to_field}')\n\n    # Validate required fields (if not an update)\n    if not is_update:\n        for f in required_fields:\n            if f not in headers:\n                raise forms.ValidationError(f'Required column header \"{f}\" not found.')\n", "path": "netbox/utilities/forms/utils.py"}], "after_files": [{"content": "import re\n\nfrom django import forms\nfrom django.forms.models import fields_for_model\n\nfrom utilities.choices import unpack_grouped_choices\nfrom utilities.querysets import RestrictedQuerySet\nfrom .constants import *\n\n__all__ = (\n    'add_blank_choice',\n    'expand_alphanumeric_pattern',\n    'expand_ipaddress_pattern',\n    'form_from_model',\n    'get_field_value',\n    'get_selected_values',\n    'parse_alphanumeric_range',\n    'parse_numeric_range',\n    'restrict_form_fields',\n    'parse_csv',\n    'validate_csv',\n)\n\n\ndef parse_numeric_range(string, base=10):\n    \"\"\"\n    Expand a numeric range (continuous or not) into a decimal or\n    hexadecimal list, as specified by the base parameter\n      '0-3,5' => [0, 1, 2, 3, 5]\n      '2,8-b,d,f' => [2, 8, 9, a, b, d, f]\n    \"\"\"\n    values = list()\n    for dash_range in string.split(','):\n        try:\n            begin, end = dash_range.split('-')\n        except ValueError:\n            begin, end = dash_range, dash_range\n        try:\n            begin, end = int(begin.strip(), base=base), int(end.strip(), base=base) + 1\n        except ValueError:\n            raise forms.ValidationError(f'Range \"{dash_range}\" is invalid.')\n        values.extend(range(begin, end))\n    return sorted(set(values))\n\n\ndef parse_alphanumeric_range(string):\n    \"\"\"\n    Expand an alphanumeric range (continuous or not) into a list.\n    'a-d,f' => [a, b, c, d, f]\n    '0-3,a-d' => [0, 1, 2, 3, a, b, c, d]\n    \"\"\"\n    values = []\n    for dash_range in string.split(','):\n        try:\n            begin, end = dash_range.split('-')\n            vals = begin + end\n            # Break out of loop if there's an invalid pattern to return an error\n            if (not (vals.isdigit() or vals.isalpha())) or (vals.isalpha() and not (vals.isupper() or vals.islower())):\n                return []\n        except ValueError:\n            begin, end = dash_range, dash_range\n        if begin.isdigit() and end.isdigit():\n            if int(begin) >= int(end):\n                raise forms.ValidationError(f'Range \"{dash_range}\" is invalid.')\n\n            for n in list(range(int(begin), int(end) + 1)):\n                values.append(n)\n        else:\n            # Value-based\n            if begin == end:\n                values.append(begin)\n            # Range-based\n            else:\n                # Not a valid range (more than a single character)\n                if not len(begin) == len(end) == 1:\n                    raise forms.ValidationError(f'Range \"{dash_range}\" is invalid.')\n\n                if ord(begin) >= ord(end):\n                    raise forms.ValidationError(f'Range \"{dash_range}\" is invalid.')\n\n                for n in list(range(ord(begin), ord(end) + 1)):\n                    values.append(chr(n))\n    return values\n\n\ndef expand_alphanumeric_pattern(string):\n    \"\"\"\n    Expand an alphabetic pattern into a list of strings.\n    \"\"\"\n    lead, pattern, remnant = re.split(ALPHANUMERIC_EXPANSION_PATTERN, string, maxsplit=1)\n    parsed_range = parse_alphanumeric_range(pattern)\n    for i in parsed_range:\n        if re.search(ALPHANUMERIC_EXPANSION_PATTERN, remnant):\n            for string in expand_alphanumeric_pattern(remnant):\n                yield \"{}{}{}\".format(lead, i, string)\n        else:\n            yield \"{}{}{}\".format(lead, i, remnant)\n\n\ndef expand_ipaddress_pattern(string, family):\n    \"\"\"\n    Expand an IP address pattern into a list of strings. Examples:\n      '192.0.2.[1,2,100-250]/24' => ['192.0.2.1/24', '192.0.2.2/24', '192.0.2.100/24' ... '192.0.2.250/24']\n      '2001:db8:0:[0,fd-ff]::/64' => ['2001:db8:0:0::/64', '2001:db8:0:fd::/64', ... '2001:db8:0:ff::/64']\n    \"\"\"\n    if family not in [4, 6]:\n        raise Exception(\"Invalid IP address family: {}\".format(family))\n    if family == 4:\n        regex = IP4_EXPANSION_PATTERN\n        base = 10\n    else:\n        regex = IP6_EXPANSION_PATTERN\n        base = 16\n    lead, pattern, remnant = re.split(regex, string, maxsplit=1)\n    parsed_range = parse_numeric_range(pattern, base)\n    for i in parsed_range:\n        if re.search(regex, remnant):\n            for string in expand_ipaddress_pattern(remnant, family):\n                yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), string])\n        else:\n            yield ''.join([lead, format(i, 'x' if family == 6 else 'd'), remnant])\n\n\ndef get_field_value(form, field_name):\n    \"\"\"\n    Return the current bound or initial value associated with a form field, prior to calling\n    clean() for the form.\n    \"\"\"\n    field = form.fields[field_name]\n\n    if form.is_bound:\n        if data := form.data.get(field_name):\n            if field.valid_value(data):\n                return data\n\n    return form.get_initial_for_field(field, field_name)\n\n\ndef get_selected_values(form, field_name):\n    \"\"\"\n    Return the list of selected human-friendly values for a form field\n    \"\"\"\n    if not hasattr(form, 'cleaned_data'):\n        form.is_valid()\n    filter_data = form.cleaned_data.get(field_name)\n    field = form.fields[field_name]\n\n    # Non-selection field\n    if not hasattr(field, 'choices'):\n        return [str(filter_data)]\n\n    # Model choice field\n    if type(field.choices) is forms.models.ModelChoiceIterator:\n        # If this is a single-choice field, wrap its value in a list\n        if not hasattr(filter_data, '__iter__'):\n            values = [filter_data]\n        else:\n            values = filter_data\n\n    else:\n        # Static selection field\n        choices = unpack_grouped_choices(field.choices)\n        if type(filter_data) not in (list, tuple):\n            filter_data = [filter_data]  # Ensure filter data is iterable\n        values = [\n            label for value, label in choices if str(value) in filter_data or None in filter_data\n        ]\n\n    # If the field has a `null_option` attribute set and it is selected,\n    # add it to the field's grouped choices.\n    if getattr(field, 'null_option', None) and None in filter_data:\n        values.remove(None)\n        values.insert(0, field.null_option)\n\n    return values\n\n\ndef add_blank_choice(choices):\n    \"\"\"\n    Add a blank choice to the beginning of a choices list.\n    \"\"\"\n    return ((None, '---------'),) + tuple(choices)\n\n\ndef form_from_model(model, fields):\n    \"\"\"\n    Return a Form class with the specified fields derived from a model. This is useful when we need a form to be used\n    for creating objects, but want to avoid the model's validation (e.g. for bulk create/edit functions). All fields\n    are marked as not required.\n    \"\"\"\n    form_fields = fields_for_model(model, fields=fields)\n    for field in form_fields.values():\n        field.required = False\n\n    return type('FormFromModel', (forms.Form,), form_fields)\n\n\ndef restrict_form_fields(form, user, action='view'):\n    \"\"\"\n    Restrict all form fields which reference a RestrictedQuerySet. This ensures that users see only permitted objects\n    as available choices.\n    \"\"\"\n    for field in form.fields.values():\n        if hasattr(field, 'queryset') and issubclass(field.queryset.__class__, RestrictedQuerySet):\n            field.queryset = field.queryset.restrict(user, action)\n\n\ndef parse_csv(reader):\n    \"\"\"\n    Parse a csv_reader object into a headers dictionary and a list of records dictionaries. Raise an error\n    if the records are formatted incorrectly. Return headers and records as a tuple.\n    \"\"\"\n    records = []\n    headers = {}\n\n    # Consume the first line of CSV data as column headers. Create a dictionary mapping each header to an optional\n    # \"to\" field specifying how the related object is being referenced. For example, importing a Device might use a\n    # `site.slug` header, to indicate the related site is being referenced by its slug.\n\n    for header in next(reader):\n        header = header.strip()\n        if '.' in header:\n            field, to_field = header.split('.', 1)\n            if field in headers:\n                raise forms.ValidationError(f'Duplicate or conflicting column header for \"{field}\"')\n            headers[field] = to_field\n        else:\n            if header in headers:\n                raise forms.ValidationError(f'Duplicate or conflicting column header for \"{header}\"')\n            headers[header] = None\n\n    # Parse CSV rows into a list of dictionaries mapped from the column headers.\n    for i, row in enumerate(reader, start=1):\n        if len(row) != len(headers):\n            raise forms.ValidationError(\n                f\"Row {i}: Expected {len(headers)} columns but found {len(row)}\"\n            )\n        row = [col.strip() for col in row]\n        record = dict(zip(headers.keys(), row))\n        records.append(record)\n\n    return headers, records\n\n\ndef validate_csv(headers, fields, required_fields):\n    \"\"\"\n    Validate that parsed csv data conforms to the object's available fields. Raise validation errors\n    if parsed csv data contains invalid headers or does not contain required headers.\n    \"\"\"\n    # Validate provided column headers\n    is_update = False\n    for field, to_field in headers.items():\n        if field == \"id\":\n            is_update = True\n            continue\n        if field not in fields:\n            raise forms.ValidationError(f'Unexpected column header \"{field}\" found.')\n        if to_field and not hasattr(fields[field], 'to_field_name'):\n            raise forms.ValidationError(f'Column \"{field}\" is not a related object; cannot use dots')\n        if to_field and not hasattr(fields[field].queryset.model, to_field):\n            raise forms.ValidationError(f'Invalid related object attribute for column \"{field}\": {to_field}')\n\n    # Validate required fields (if not an update)\n    if not is_update:\n        for f in required_fields:\n            if f not in headers:\n                raise forms.ValidationError(f'Required column header \"{f}\" not found.')\n", "path": "netbox/utilities/forms/utils.py"}]}