mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-26 09:16:10 -06:00
Merge branch 'develop' into fix/12627-image-preview
This commit is contained in:
commit
4f6bc2b3da
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#11934](https://github.com/netbox-community/netbox/issues/11934) - Prevent reassignment of an IP address designated as primary for its parent object
|
||||||
* [#12694](https://github.com/netbox-community/netbox/issues/12694) - Strip leading & trailing whitespace from custom link URL & text
|
* [#12694](https://github.com/netbox-community/netbox/issues/12694) - Strip leading & trailing whitespace from custom link URL & text
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -7,12 +7,14 @@ class Empty(Lookup):
|
|||||||
Filter on whether a string is empty.
|
Filter on whether a string is empty.
|
||||||
"""
|
"""
|
||||||
lookup_name = 'empty'
|
lookup_name = 'empty'
|
||||||
|
prepare_rhs = False
|
||||||
|
|
||||||
def as_sql(self, qn, connection):
|
def as_sql(self, compiler, connection):
|
||||||
lhs, lhs_params = self.process_lhs(qn, connection)
|
sql, params = compiler.compile(self.lhs)
|
||||||
rhs, rhs_params = self.process_rhs(qn, connection)
|
if self.rhs:
|
||||||
params = lhs_params + rhs_params
|
return f"CAST(LENGTH({sql}) AS BOOLEAN) IS NOT TRUE", params
|
||||||
return 'CAST(LENGTH(%s) AS BOOLEAN) != %s' % (lhs, rhs), params
|
else:
|
||||||
|
return f"CAST(LENGTH({sql}) AS BOOLEAN) IS TRUE", params
|
||||||
|
|
||||||
|
|
||||||
class NetContainsOrEquals(Lookup):
|
class NetContainsOrEquals(Lookup):
|
||||||
|
@ -328,6 +328,12 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
|||||||
):
|
):
|
||||||
self.initial['primary_for_parent'] = True
|
self.initial['primary_for_parent'] = True
|
||||||
|
|
||||||
|
# Disable object assignment fields if the IP address is designated as primary
|
||||||
|
if self.initial.get('primary_for_parent'):
|
||||||
|
self.fields['interface'].disabled = True
|
||||||
|
self.fields['vminterface'].disabled = True
|
||||||
|
self.fields['fhrpgroup'].disabled = True
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
@ -340,7 +346,12 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
|||||||
selected_objects[1]: "An IP address can only be assigned to a single object."
|
selected_objects[1]: "An IP address can only be assigned to a single object."
|
||||||
})
|
})
|
||||||
elif selected_objects:
|
elif selected_objects:
|
||||||
self.instance.assigned_object = self.cleaned_data[selected_objects[0]]
|
assigned_object = self.cleaned_data[selected_objects[0]]
|
||||||
|
if self.cleaned_data['primary_for_parent'] and assigned_object != self.instance.assigned_object:
|
||||||
|
raise ValidationError(
|
||||||
|
"Cannot reassign IP address while it is designated as the primary IP for the parent object"
|
||||||
|
)
|
||||||
|
self.instance.assigned_object = assigned_object
|
||||||
else:
|
else:
|
||||||
self.instance.assigned_object = None
|
self.instance.assigned_object = None
|
||||||
|
|
||||||
|
@ -177,7 +177,8 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
# create the new filter with the same type because there is no guarantee the defined type
|
# create the new filter with the same type because there is no guarantee the defined type
|
||||||
# is the same as the default type for the field
|
# is the same as the default type for the field
|
||||||
resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
|
resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
|
||||||
new_filter = type(existing_filter)(
|
filter_cls = django_filters.BooleanFilter if lookup_expr == 'empty' else type(existing_filter)
|
||||||
|
new_filter = filter_cls(
|
||||||
field_name=field_name,
|
field_name=field_name,
|
||||||
lookup_expr=lookup_expr,
|
lookup_expr=lookup_expr,
|
||||||
label=existing_filter.label,
|
label=existing_filter.label,
|
||||||
@ -224,6 +225,14 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||||||
|
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def filter_for_lookup(cls, field, lookup_type):
|
||||||
|
|
||||||
|
if lookup_type == 'empty':
|
||||||
|
return django_filters.BooleanFilter, {}
|
||||||
|
|
||||||
|
return super().filter_for_lookup(field, lookup_type)
|
||||||
|
|
||||||
|
|
||||||
class ChangeLoggedModelFilterSet(BaseFilterSet):
|
class ChangeLoggedModelFilterSet(BaseFilterSet):
|
||||||
"""
|
"""
|
||||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -30,6 +30,7 @@
|
|||||||
"dayjs": "^1.11.5",
|
"dayjs": "^1.11.5",
|
||||||
"flatpickr": "4.6.13",
|
"flatpickr": "4.6.13",
|
||||||
"gridstack": "^7.2.3",
|
"gridstack": "^7.2.3",
|
||||||
|
"html-entities": "^2.3.3",
|
||||||
"htmx.org": "^1.8.0",
|
"htmx.org": "^1.8.0",
|
||||||
"just-debounce-it": "^3.1.1",
|
"just-debounce-it": "^3.1.1",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.1",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { readableColor } from 'color2k';
|
import { readableColor } from 'color2k';
|
||||||
import debounce from 'just-debounce-it';
|
import debounce from 'just-debounce-it';
|
||||||
|
import { encode } from 'html-entities';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import SlimSelect from 'slim-select';
|
import SlimSelect from 'slim-select';
|
||||||
import { createToast } from '../../bs';
|
import { createToast } from '../../bs';
|
||||||
@ -446,7 +447,7 @@ export class APISelect {
|
|||||||
// Build SlimSelect options from all already-selected options.
|
// Build SlimSelect options from all already-selected options.
|
||||||
const preSelectedOptions = preSelected.map(option => ({
|
const preSelectedOptions = preSelected.map(option => ({
|
||||||
value: option.value,
|
value: option.value,
|
||||||
text: option.innerText,
|
text: encode(option.innerText),
|
||||||
selected: true,
|
selected: true,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
})) as Option[];
|
})) as Option[];
|
||||||
@ -454,7 +455,7 @@ export class APISelect {
|
|||||||
let options = [] as Option[];
|
let options = [] as Option[];
|
||||||
|
|
||||||
for (const result of data.results) {
|
for (const result of data.results) {
|
||||||
let text = result.display;
|
let text = encode(result.display);
|
||||||
|
|
||||||
if (typeof result._depth === 'number' && result._depth > 0) {
|
if (typeof result._depth === 'number' && result._depth > 0) {
|
||||||
// If the object has a `_depth` property, indent its display text.
|
// If the object has a `_depth` property, indent its display text.
|
||||||
|
@ -1818,6 +1818,11 @@ has@^1.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.1"
|
function-bind "^1.1.1"
|
||||||
|
|
||||||
|
html-entities@^2.3.3:
|
||||||
|
version "2.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46"
|
||||||
|
integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==
|
||||||
|
|
||||||
htmx.org@^1.8.0:
|
htmx.org@^1.8.0:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/htmx.org/-/htmx.org-1.8.0.tgz#f3a2f681f3e2b6357b5a29bba24a2572a8e48fd3"
|
resolved "https://registry.yarnpkg.com/htmx.org/-/htmx.org-1.8.0.tgz#f3a2f681f3e2b6357b5a29bba24a2572a8e48fd3"
|
||||||
|
@ -28,11 +28,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-7">
|
<div class="col-7">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">Context Data</h5>
|
<div class="accordion accordion-flush" id="renderConfig">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="renderConfigHeading">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsedRenderConfig" aria-expanded="false" aria-controls="collapsedRenderConfig">
|
||||||
|
Context Data
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapsedRenderConfig" class="accordion-collapse collapse" aria-labelledby="renderConfigHeading" data-bs-parent="#renderConfig">
|
||||||
|
<div class="accordion-body">
|
||||||
<pre class="card-body">{{ context_data|pprint }}</pre>
|
<pre class="card-body">{{ context_data|pprint }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
@ -29,7 +29,10 @@ class ObjectContactsView(generic.ObjectChildrenView):
|
|||||||
def get_children(self, request, parent):
|
def get_children(self, request, parent):
|
||||||
return Contact.objects.annotate(
|
return Contact.objects.annotate(
|
||||||
assignment_count=count_related(ContactAssignment, 'contact')
|
assignment_count=count_related(ContactAssignment, 'contact')
|
||||||
).restrict(request.user, 'view').filter(assignments__object_id=parent.pk)
|
).restrict(request.user, 'view').filter(
|
||||||
|
assignments__content_type=ContentType.objects.get_for_model(parent),
|
||||||
|
assignments__object_id=parent.pk
|
||||||
|
)
|
||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
return {
|
return {
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import logging
|
import logging
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.contrib.auth.signals import user_login_failed
|
from django.contrib.auth.signals import user_login_failed
|
||||||
|
from utilities.request import get_client_ip
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_login_failed)
|
@receiver(user_login_failed)
|
||||||
def log_user_login_failed(sender, credentials, request, **kwargs):
|
def log_user_login_failed(sender, credentials, request, **kwargs):
|
||||||
logger = logging.getLogger('netbox.auth.login')
|
logger = logging.getLogger('netbox.auth.login')
|
||||||
username = credentials.get("username")
|
username = credentials.get("username")
|
||||||
|
if client_ip := get_client_ip(request):
|
||||||
|
logger.info(f"Failed login attempt for username: {username} from {client_ip}")
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Client IP address could not be determined for validation. Check that the HTTP server is properly "
|
||||||
|
"configured to pass the required header(s)."
|
||||||
|
)
|
||||||
logger.info(f"Failed login attempt for username: {username}")
|
logger.info(f"Failed login attempt for username: {username}")
|
||||||
|
Loading…
Reference in New Issue
Block a user