Merge pull request #7984 from netbox-community/develop

Release v3.0.12
This commit is contained in:
Jeremy Stretch 2021-12-06 12:07:04 -05:00 committed by GitHub
commit b7129e1456
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 94 additions and 17 deletions

View File

@ -14,7 +14,7 @@ body:
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v3.0.11 placeholder: v3.0.12
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v3.0.11 placeholder: v3.0.12
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -1,5 +1,22 @@
# NetBox v3.0 # NetBox v3.0
## v3.0.12 (2021-12-06)
### Enhancements
* [#7751](https://github.com/netbox-community/netbox/issues/7751) - Get API user from LDAP only when `FIND_GROUP_PERMS` is enabled
* [#7885](https://github.com/netbox-community/netbox/issues/7885) - Linkify VLAN name in VLANs table
* [#7892](https://github.com/netbox-community/netbox/issues/7892) - Add L22-30 power port & outlet types
* [#7932](https://github.com/netbox-community/netbox/issues/7932) - Improve performance of the "quick find" function
* [#7941](https://github.com/netbox-community/netbox/issues/7941) - Add multi-standard ITA power outlet type
### Bug Fixes
* [#7823](https://github.com/netbox-community/netbox/issues/7823) - Fix issue where `return_url` is not honored when 'Save & Continue' button is present
* [#7981](https://github.com/netbox-community/netbox/issues/7981) - Fix Markdown sanitization regex
---
## v3.0.11 (2021-11-24) ## v3.0.11 (2021-11-24)
### Enhancements ### Enhancements

View File

@ -312,6 +312,7 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_NEMA_L1560P = 'nema-l15-60p' TYPE_NEMA_L1560P = 'nema-l15-60p'
TYPE_NEMA_L2120P = 'nema-l21-20p' TYPE_NEMA_L2120P = 'nema-l21-20p'
TYPE_NEMA_L2130P = 'nema-l21-30p' TYPE_NEMA_L2130P = 'nema-l21-30p'
TYPE_NEMA_L2230P = 'nema-l22-30p'
# California style # California style
TYPE_CS6361C = 'cs6361c' TYPE_CS6361C = 'cs6361c'
TYPE_CS6365C = 'cs6365c' TYPE_CS6365C = 'cs6365c'
@ -417,6 +418,7 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_NEMA_L1560P, 'NEMA L15-60P'), (TYPE_NEMA_L1560P, 'NEMA L15-60P'),
(TYPE_NEMA_L2120P, 'NEMA L21-20P'), (TYPE_NEMA_L2120P, 'NEMA L21-20P'),
(TYPE_NEMA_L2130P, 'NEMA L21-30P'), (TYPE_NEMA_L2130P, 'NEMA L21-30P'),
(TYPE_NEMA_L2230P, 'NEMA L22-30P'),
)), )),
('California Style', ( ('California Style', (
(TYPE_CS6361C, 'CS6361C'), (TYPE_CS6361C, 'CS6361C'),
@ -533,6 +535,7 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_NEMA_L1560R = 'nema-l15-60r' TYPE_NEMA_L1560R = 'nema-l15-60r'
TYPE_NEMA_L2120R = 'nema-l21-20r' TYPE_NEMA_L2120R = 'nema-l21-20r'
TYPE_NEMA_L2130R = 'nema-l21-30r' TYPE_NEMA_L2130R = 'nema-l21-30r'
TYPE_NEMA_L2230R = 'nema-l22-30r'
# California style # California style
TYPE_CS6360C = 'CS6360C' TYPE_CS6360C = 'CS6360C'
TYPE_CS6364C = 'CS6364C' TYPE_CS6364C = 'CS6364C'
@ -552,6 +555,7 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_ITA_M = 'ita-m' TYPE_ITA_M = 'ita-m'
TYPE_ITA_N = 'ita-n' TYPE_ITA_N = 'ita-n'
TYPE_ITA_O = 'ita-o' TYPE_ITA_O = 'ita-o'
TYPE_ITA_MULTISTANDARD = 'ita-multistandard'
# USB # USB
TYPE_USB_A = 'usb-a' TYPE_USB_A = 'usb-a'
TYPE_USB_MICROB = 'usb-micro-b' TYPE_USB_MICROB = 'usb-micro-b'
@ -630,6 +634,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_NEMA_L1560R, 'NEMA L15-60R'), (TYPE_NEMA_L1560R, 'NEMA L15-60R'),
(TYPE_NEMA_L2120R, 'NEMA L21-20R'), (TYPE_NEMA_L2120R, 'NEMA L21-20R'),
(TYPE_NEMA_L2130R, 'NEMA L21-30R'), (TYPE_NEMA_L2130R, 'NEMA L21-30R'),
(TYPE_NEMA_L2230R, 'NEMA L22-30R'),
)), )),
('California Style', ( ('California Style', (
(TYPE_CS6360C, 'CS6360C'), (TYPE_CS6360C, 'CS6360C'),
@ -651,6 +656,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_ITA_M, 'ITA Type M (BS 546)'), (TYPE_ITA_M, 'ITA Type M (BS 546)'),
(TYPE_ITA_N, 'ITA Type N'), (TYPE_ITA_N, 'ITA Type N'),
(TYPE_ITA_O, 'ITA Type O'), (TYPE_ITA_O, 'ITA Type O'),
(TYPE_ITA_MULTISTANDARD, 'ITA Multistandard'),
)), )),
('USB', ( ('USB', (
(TYPE_USB_A, 'USB Type A'), (TYPE_USB_A, 'USB Type A'),

View File

@ -95,6 +95,9 @@ class VLANTable(BaseTable):
template_code=VLAN_LINK, template_code=VLAN_LINK,
verbose_name='VID' verbose_name='VID'
) )
name = tables.Column(
linkify=True
)
site = tables.Column( site = tables.Column(
linkify=True linkify=True
) )

View File

@ -29,6 +29,9 @@ class TokenAuthentication(authentication.TokenAuthentication):
if settings.REMOTE_AUTH_BACKEND == 'netbox.authentication.LDAPBackend': if settings.REMOTE_AUTH_BACKEND == 'netbox.authentication.LDAPBackend':
from netbox.authentication import LDAPBackend from netbox.authentication import LDAPBackend
ldap_backend = LDAPBackend() ldap_backend = LDAPBackend()
# Load from LDAP if FIND_GROUP_PERMS is active
if ldap_backend.settings.FIND_GROUP_PERMS:
user = ldap_backend.populate_user(token.user.username) user = ldap_backend.populate_user(token.user.username)
# If the user is found in the LDAP directory use it, if not fallback to the local user # If the user is found in the LDAP directory use it, if not fallback to the local user
if user: if user:

View File

@ -17,7 +17,7 @@ from django.core.validators import URLValidator
# Environment setup # Environment setup
# #
VERSION = '3.0.11' VERSION = '3.0.12'
# Hostname # Hostname
HOSTNAME = platform.node() HOSTNAME = platform.node()

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +1,32 @@
import { getElements, scrollTo } from '../util'; import { getElements, scrollTo, isTruthy } from '../util';
/**
* When editing an object, it is sometimes desirable to customize the form action *without*
* overriding the form's `submit` event. For example, the 'Save & Continue' button. We don't want
* to use the `formaction` attribute on that element because it will be included on the form even
* if the button isn't clicked.
*
* @example
* ```html
* <button type="button" return-url="/special-url/">
* Save & Continue
* </button>
* ```
*
* @param event Click event.
*/
function handleSubmitWithReturnUrl(event: MouseEvent): void {
const element = event.target as HTMLElement;
if (element.tagName === 'BUTTON') {
const button = element as HTMLButtonElement;
const action = button.getAttribute('return-url');
const form = button.form;
if (form !== null && isTruthy(action)) {
form.action = action;
form.submit();
}
}
}
function handleFormSubmit(event: Event, form: HTMLFormElement): void { function handleFormSubmit(event: Event, form: HTMLFormElement): void {
// Track the names of each invalid field. // Track the names of each invalid field.
@ -38,6 +66,15 @@ function handleFormSubmit(event: Event, form: HTMLFormElement): void {
} }
} }
/**
* Attach event listeners to form buttons with the `return-url` attribute present.
*/
function initReturnUrlSubmitButtons(): void {
for (const button of getElements<HTMLButtonElement>('button[return-url]')) {
button.addEventListener('click', handleSubmitWithReturnUrl);
}
}
/** /**
* Attach an event listener to each form's submitter (button[type=submit]). When called, the * Attach an event listener to each form's submitter (button[type=submit]). When called, the
* callback checks the validity of each form field and adds the appropriate Bootstrap CSS class * callback checks the validity of each form field and adds the appropriate Bootstrap CSS class
@ -54,4 +91,5 @@ export function initFormElements(): void {
submitter.addEventListener('click', (event: Event) => handleFormSubmit(event, form)); submitter.addEventListener('click', (event: Event) => handleFormSubmit(event, form));
} }
} }
initReturnUrlSubmitButtons();
} }

View File

@ -107,6 +107,9 @@ function initTableFilter(): void {
// Create a regex pattern from the input search text to match against. // Create a regex pattern from the input search text to match against.
const filter = new RegExp(target.value.toLowerCase().trim()); const filter = new RegExp(target.value.toLowerCase().trim());
// List of which rows which match the query
const matchedRows: Array<HTMLTableRowElement> = [];
for (const row of rows) { for (const row of rows) {
// Find the row's checkbox and deselect it, so that it is not accidentally included in form // Find the row's checkbox and deselect it, so that it is not accidentally included in form
// submissions. // submissions.
@ -114,19 +117,26 @@ function initTableFilter(): void {
if (checkBox !== null) { if (checkBox !== null) {
checkBox.checked = false; checkBox.checked = false;
} }
// Iterate through each row's cell values // Iterate through each row's cell values
for (const value of getRowValues(row)) { for (const value of getRowValues(row)) {
if (filter.test(value.toLowerCase())) { if (filter.test(value.toLowerCase())) {
// If this row matches the search pattern, but is already hidden, unhide it and stop // If this row matches the search pattern, add it to the list.
// iterating through the rest of the cells. matchedRows.push(row);
row.classList.remove('d-none');
break; break;
} else {
// If none of the cells in this row match the search pattern, hide the row.
row.classList.add('d-none');
} }
} }
} }
// Iterate the rows again to set visibility.
// This results in a single reflow instead of one for each row.
for (const row of rows) {
if (matchedRows.indexOf(row) >= 0) {
row.classList.remove('d-none');
} else {
row.classList.add('d-none');
}
}
} }
input.addEventListener('keyup', debounce(handleInput, 300)); input.addEventListener('keyup', debounce(handleInput, 300));
} }

View File

@ -51,7 +51,7 @@
{% block buttons %} {% block buttons %}
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a> <a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
{% if obj.pk %} {% if obj.pk %}
<button type="submit" formaction="?return_url={% url 'dcim:interface_edit' pk=obj.pk %}" class="btn btn-outline-primary">Save & Continue Editing</button> <button type="button" return-url="?return_url={% url 'dcim:interface_edit' pk=obj.pk %}" class="btn btn-outline-primary">Save & Continue Editing</button>
<button type="submit" name="_update" class="btn btn-primary">Save</button> <button type="submit" name="_update" class="btn btn-primary">Save</button>
{% else %} {% else %}
<button type="submit" name="_addanother" class="btn btn-outline-primary">Create & Add Another</button> <button type="submit" name="_addanother" class="btn btn-outline-primary">Create & Add Another</button>

View File

@ -46,7 +46,7 @@
{% block buttons %} {% block buttons %}
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a> <a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
{% if obj.pk %} {% if obj.pk %}
<button type="submit" formaction="?return_url={% url 'virtualization:vminterface_edit' pk=obj.pk %}" class="btn btn-outline-primary">Save & Continue Editing</button> <button type="button" return-url="?return_url={% url 'virtualization:vminterface_edit' pk=obj.pk %}" class="btn btn-outline-primary">Save & Continue Editing</button>
<button type="submit" name="_update" class="btn btn-primary">Save</button> <button type="submit" name="_update" class="btn btn-primary">Save</button>
{% else %} {% else %}
<button type="submit" name="_addanother" class="btn btn-outline-primary">Create & Add Another</button> <button type="submit" name="_addanother" class="btn btn-outline-primary">Create & Add Another</button>

View File

@ -51,7 +51,7 @@ def render_markdown(value):
value = re.sub(pattern, '[\\1](\\3)', value, flags=re.IGNORECASE) value = re.sub(pattern, '[\\1](\\3)', value, flags=re.IGNORECASE)
# Sanitize Markdown reference links # Sanitize Markdown reference links
pattern = fr'\[(.+)\]:\w?(?!({schemes})).*:(.+)' pattern = fr'\[(.+)\]:\s*(?!({schemes}))\w*:(.+)'
value = re.sub(pattern, '[\\1]: \\3', value, flags=re.IGNORECASE) value = re.sub(pattern, '[\\1]: \\3', value, flags=re.IGNORECASE)
# Render Markdown # Render Markdown