Merge branch 'develop' into fix/12627-image-preview

This commit is contained in:
jeremystretch 2023-05-30 09:14:44 -04:00
commit 4f6bc2b3da
12 changed files with 68 additions and 13 deletions

View File

@ -4,6 +4,7 @@
### 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
---

View File

@ -7,12 +7,14 @@ class Empty(Lookup):
Filter on whether a string is empty.
"""
lookup_name = 'empty'
prepare_rhs = False
def as_sql(self, qn, connection):
lhs, lhs_params = self.process_lhs(qn, connection)
rhs, rhs_params = self.process_rhs(qn, connection)
params = lhs_params + rhs_params
return 'CAST(LENGTH(%s) AS BOOLEAN) != %s' % (lhs, rhs), params
def as_sql(self, compiler, connection):
sql, params = compiler.compile(self.lhs)
if self.rhs:
return f"CAST(LENGTH({sql}) AS BOOLEAN) IS NOT TRUE", params
else:
return f"CAST(LENGTH({sql}) AS BOOLEAN) IS TRUE", params
class NetContainsOrEquals(Lookup):

View File

@ -328,6 +328,12 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
):
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):
super().clean()
@ -340,7 +346,12 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
selected_objects[1]: "An IP address can only be assigned to a single object."
})
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:
self.instance.assigned_object = None

View File

@ -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
# is the same as the default type for the field
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,
lookup_expr=lookup_expr,
label=existing_filter.label,
@ -224,6 +225,14 @@ class BaseFilterSet(django_filters.FilterSet):
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):
"""

Binary file not shown.

Binary file not shown.

View File

@ -30,6 +30,7 @@
"dayjs": "^1.11.5",
"flatpickr": "4.6.13",
"gridstack": "^7.2.3",
"html-entities": "^2.3.3",
"htmx.org": "^1.8.0",
"just-debounce-it": "^3.1.1",
"query-string": "^7.1.1",

View File

@ -1,5 +1,6 @@
import { readableColor } from 'color2k';
import debounce from 'just-debounce-it';
import { encode } from 'html-entities';
import queryString from 'query-string';
import SlimSelect from 'slim-select';
import { createToast } from '../../bs';
@ -446,7 +447,7 @@ export class APISelect {
// Build SlimSelect options from all already-selected options.
const preSelectedOptions = preSelected.map(option => ({
value: option.value,
text: option.innerText,
text: encode(option.innerText),
selected: true,
disabled: false,
})) as Option[];
@ -454,7 +455,7 @@ export class APISelect {
let options = [] as Option[];
for (const result of data.results) {
let text = result.display;
let text = encode(result.display);
if (typeof result._depth === 'number' && result._depth > 0) {
// If the object has a `_depth` property, indent its display text.

View File

@ -1818,6 +1818,11 @@ has@^1.0.3:
dependencies:
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:
version "1.8.0"
resolved "https://registry.yarnpkg.com/htmx.org/-/htmx.org-1.8.0.tgz#f3a2f681f3e2b6357b5a29bba24a2572a8e48fd3"

View File

@ -28,8 +28,22 @@
</div>
<div class="col-7">
<div class="card">
<h5 class="card-header">Context Data</h5>
<pre class="card-body">{{ context_data|pprint }}</pre>
<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>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -29,7 +29,10 @@ class ObjectContactsView(generic.ObjectChildrenView):
def get_children(self, request, parent):
return Contact.objects.annotate(
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):
return {

View File

@ -1,10 +1,18 @@
import logging
from django.dispatch import receiver
from django.contrib.auth.signals import user_login_failed
from utilities.request import get_client_ip
@receiver(user_login_failed)
def log_user_login_failed(sender, credentials, request, **kwargs):
logger = logging.getLogger('netbox.auth.login')
username = credentials.get("username")
logger.info(f"Failed login attempt for username: {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}")