mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-14 07:42:18 -06:00
Compare commits
4 Commits
main
...
20911-drop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24642be351 | ||
|
|
89af9efd85 | ||
|
|
99d678502f | ||
|
|
e6300ee06d |
16
.github/ISSUE_TEMPLATE/06-deprecation.yaml
vendored
16
.github/ISSUE_TEMPLATE/06-deprecation.yaml
vendored
@@ -1,26 +1,20 @@
|
|||||||
---
|
---
|
||||||
name: ⚠️ Deprecation
|
name: 🗑️ Deprecation
|
||||||
type: Deprecation
|
type: Deprecation
|
||||||
description: Designation of a feature or behavior that will be removed in a future release
|
description: The removal of an existing feature or resource
|
||||||
labels: ["netbox", "type: deprecation"]
|
labels: ["netbox", "type: deprecation"]
|
||||||
body:
|
body:
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Deprecated Functionality
|
label: Proposed Changes
|
||||||
description: >
|
description: >
|
||||||
Describe the feature(s) and/or behavior that is being flagged for deprecation.
|
Describe in detail the proposed changes. What is being removed?
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
attributes:
|
|
||||||
label: Scheduled removal
|
|
||||||
description: In what future release will the deprecated functionality be removed?
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Justification
|
label: Justification
|
||||||
description: Please provide justification for the deprecation.
|
description: Please provide justification for the proposed change(s).
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
20
.github/ISSUE_TEMPLATE/07-feature_removal.yaml
vendored
20
.github/ISSUE_TEMPLATE/07-feature_removal.yaml
vendored
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: 🗑️ Feature Removal
|
|
||||||
type: Removal
|
|
||||||
description: The removal of a deprecated feature or resource
|
|
||||||
labels: ["netbox", "type: removal"]
|
|
||||||
body:
|
|
||||||
- type: input
|
|
||||||
attributes:
|
|
||||||
label: Deprecation Issue
|
|
||||||
description: Specify the issue in which this deprecation was announced.
|
|
||||||
placeholder: "#1234"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Summary of Changes
|
|
||||||
description: >
|
|
||||||
List all changes necessary to remove the deprecated feature or resource.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,8 +9,7 @@ yarn-error.log*
|
|||||||
/netbox/netbox/configuration.py
|
/netbox/netbox/configuration.py
|
||||||
/netbox/netbox/ldap_config.py
|
/netbox/netbox/ldap_config.py
|
||||||
/netbox/local/*
|
/netbox/local/*
|
||||||
/netbox/media/*
|
/netbox/media
|
||||||
!/netbox/media/.gitkeep
|
|
||||||
/netbox/reports/*
|
/netbox/reports/*
|
||||||
!/netbox/reports/__init__.py
|
!/netbox/reports/__init__.py
|
||||||
/netbox/scripts/*
|
/netbox/scripts/*
|
||||||
|
|||||||
@@ -10,11 +10,9 @@ Change records are exposed in the API via the read-only endpoint `/api/extras/ob
|
|||||||
|
|
||||||
## User Messages
|
## User Messages
|
||||||
|
|
||||||
When creating, modifying, or deleting an object in NetBox, a user has the option of recording an arbitrary message (up to 200 characters) that will appear in the change record. This can be helpful to capture additional context, such as the reason for a change or a reference to an external ticket.
|
!!! info "This feature was introduced in NetBox v4.4."
|
||||||
|
|
||||||
When editing an object via the web UI, the "Changelog message" field appears at the bottom of the form. This field is optional. The changelog message field is available in object create forms, object edit forms, delete confirmation dialogs, and bulk operations.
|
When creating, modifying, or deleting an object in NetBox, a user has the option of recording an arbitrary message that will appear in the change record. This can be helpful to capture additional context, such as the reason for the change.
|
||||||
|
|
||||||
For information on including changelog messages when making changes via the REST API, see [Changelog Messages](../integrations/rest-api.md#changelog-messages).
|
|
||||||
|
|
||||||
## Correlating Changes by Request
|
## Correlating Changes by Request
|
||||||
|
|
||||||
|
|||||||
@@ -610,7 +610,9 @@ http://netbox/api/dcim/sites/ \
|
|||||||
|
|
||||||
## Changelog Messages
|
## Changelog Messages
|
||||||
|
|
||||||
Most objects in NetBox support [change logging](../features/change-logging.md), which generates a detailed record each time an object is created, modified, or deleted. Additionally, users can attach a message to the change record as well. This is accomplished via the REST API by including a `changelog_message` field in the object representation.
|
!!! info "This feature was introduced in NetBox v4.4."
|
||||||
|
|
||||||
|
Most objects in NetBox support [change logging](../features/change-logging.md), which generates a detailed record each time an object is created, modified, or deleted. Beginning in NetBox v4.4, users can attach a message to the change record as well. This is accomplished via the REST API by including a `changelog_message` field in the object representation.
|
||||||
|
|
||||||
For example, the following API request will create a new site and record a message in the resulting changelog entry:
|
For example, the following API request will create a new site and record a message in the resulting changelog entry:
|
||||||
|
|
||||||
@@ -626,7 +628,7 @@ http://netbox/api/dcim/sites/ \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
This approach works when creating, modifying, or deleting objects, either individually or in bulk. For more information about change logging, see [Change Logging](../features/change-logging.md).
|
This approach works when creating, modifying, or deleting objects, either individually or in bulk.
|
||||||
|
|
||||||
## Uploading Files
|
## Uploading Files
|
||||||
|
|
||||||
|
|||||||
@@ -733,9 +733,10 @@ class ModuleForm(ModuleCommonForm, PrimaryModelForm):
|
|||||||
)
|
)
|
||||||
module_bay = DynamicModelChoiceField(
|
module_bay = DynamicModelChoiceField(
|
||||||
label=_('Module bay'),
|
label=_('Module bay'),
|
||||||
queryset=ModuleBay.objects.all(),
|
queryset=ModuleBay.objects.order_by('name'),
|
||||||
query_params={
|
query_params={
|
||||||
'device_id': '$device'
|
'device_id': '$device',
|
||||||
|
'ordering': 'name',
|
||||||
},
|
},
|
||||||
context={
|
context={
|
||||||
'disabled': 'installed_module',
|
'disabled': 'installed_module',
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ VPN_MENU = Menu(
|
|||||||
label=_('L2VPNs'),
|
label=_('L2VPNs'),
|
||||||
items=(
|
items=(
|
||||||
get_model_item('vpn', 'l2vpn', _('L2VPNs')),
|
get_model_item('vpn', 'l2vpn', _('L2VPNs')),
|
||||||
get_model_item('vpn', 'l2vpntermination', _('L2VPN Terminations')),
|
get_model_item('vpn', 'l2vpntermination', _('Terminations')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
.docExplorerWrap{height:unset!important;min-width:unset!important;width:unset!important}.docExplorerWrap svg{display:unset}.doc-explorer-title{font-size:var(--font-size-h2);font-weight:var(--font-weight-medium)}.doc-explorer-rhs{display:none}.graphiql-explorer-root{font-family:var(--font-family-mono)!important;font-size:var(--font-size-body)!important;padding:0!important}.graphiql-explorer-root>div>div{padding-top:var(--px-16);border-color:hsla(var(--color-neutral),var(--alpha-background-heavy))!important}.graphiql-explorer-root>div{overflow:auto!important}.graphiql-explorer-root input{background:unset}.graphiql-explorer-root select{border:1px solid hsla(var(--color-neutral),var(--alpha-secondary));border-radius:var(--border-radius-4);margin:0 var(--px-8);padding:var(--px-4)var(--px-6);background:hsl(var(--color-base))!important;color:hsl(var(--color-neutral))!important}.toolbar-button{all:unset;cursor:pointer;margin-left:var(--px-6);color:hsl(var(--color-primary));line-height:0!important;font-size:var(--font-size-h3)!important}.graphiql-explorer-slug .toolbar-button,.graphiql-explorer-graphql-arguments .toolbar-button{font-size:inherit!important}.graphiql-explorer-graphql-arguments input{min-width:2rem;line-height:0}.graphiql-explorer-actions{border-color:hsla(var(--color-neutral),var(--alpha-background-heavy))!important}
|
.docExplorerWrap{height:unset!important;min-width:unset!important;width:unset!important}.docExplorerWrap svg{display:unset}.doc-explorer-title{font-size:var(--font-size-h2);font-weight:var(--font-weight-medium)}.doc-explorer-rhs{display:none}.graphiql-explorer-root{font-family:var(--font-family-mono)!important;font-size:var(--font-size-body)!important;padding:0!important}.graphiql-explorer-root>div>div{border-color:hsla(var(--color-neutral),var(--alpha-background-heavy))!important;padding-top:var(--px-16)}.graphiql-explorer-root input{background:unset}.graphiql-explorer-root select{background:hsl(var(--color-base))!important;border:1px solid hsla(var(--color-neutral),var(--alpha-secondary));border-radius:var(--border-radius-4);color:hsl(var(--color-neutral))!important;margin:0 var(--px-8);padding:var(--px-4) var(--px-6)}.graphiql-operation-title-bar .toolbar-button{line-height:0;margin-left:var(--px-8);color:hsla(var(--color-neutral),var(--alpha-secondary, .6));font-size:var(--font-size-h3);vertical-align:middle}.graphiql-explorer-graphql-arguments input{line-height:0}.graphiql-explorer-actions{border-color:hsla(var(--color-neutral),var(--alpha-background-heavy))!important}
|
||||||
|
|||||||
10
netbox/project-static/dist/netbox.js
vendored
10
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/netbox.js.map
vendored
6
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@graphiql/plugin-explorer": "4.0.6",
|
"@graphiql/plugin-explorer": "3.2.6",
|
||||||
"graphiql": "4.1.2",
|
"graphiql": "4.1.2",
|
||||||
"graphql": "16.12.0",
|
"graphql": "16.12.0",
|
||||||
"js-cookie": "3.0.5",
|
"js-cookie": "3.0.5",
|
||||||
|
|||||||
@@ -75,11 +75,15 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
load(value: string) {
|
load(value: string) {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
|
const currentValue = self.getValue();
|
||||||
|
|
||||||
// Automatically clear any cached options. (Only options included
|
// Automatically clear any cached options. (Only options included
|
||||||
// in the API response should be present.)
|
// in the API response should be present.)
|
||||||
self.clearOptions();
|
self.clearOptions();
|
||||||
|
|
||||||
// Populate the null option (if any) if not searching
|
// Clear user_options to prevent the pre-selected option from being treated specially
|
||||||
|
(self as any).user_options = {};
|
||||||
|
|
||||||
if (self.nullOption && !value) {
|
if (self.nullOption && !value) {
|
||||||
self.addOption(self.nullOption);
|
self.addOption(self.nullOption);
|
||||||
}
|
}
|
||||||
@@ -93,21 +97,33 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
addClasses(self.wrapper, self.settings.loadingClass);
|
addClasses(self.wrapper, self.settings.loadingClass);
|
||||||
self.loading++;
|
self.loading++;
|
||||||
|
|
||||||
// Make the API request
|
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(apiData => {
|
.then(apiData => {
|
||||||
const results: Dict[] = apiData.results;
|
const results: Dict[] = apiData.results;
|
||||||
const options: Dict[] = [];
|
|
||||||
for (const result of results) {
|
// Add options and set $order to preserve API response order
|
||||||
|
results.forEach((result, index) => {
|
||||||
const option = self.getOptionFromData(result);
|
const option = self.getOptionFromData(result);
|
||||||
options.push(option);
|
self.addOption(option);
|
||||||
|
const key = option[self.settings.valueField as string] as string;
|
||||||
|
if (self.options[key]) {
|
||||||
|
(self.options[key] as any).$order = index;
|
||||||
}
|
}
|
||||||
return options;
|
});
|
||||||
})
|
|
||||||
// Pass the options to the callback function
|
if (self.loading > 0) {
|
||||||
.then(options => {
|
self.loading--;
|
||||||
self.loadCallback(options, []);
|
if (self.loading === 0) {
|
||||||
|
self.wrapper.classList.remove(self.settings.loadingClass as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentValue && !self.items.includes(currentValue as string)) {
|
||||||
|
self.items.push(currentValue as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.refreshOptions(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
self.loadCallback([], []);
|
self.loadCallback([], []);
|
||||||
|
|||||||
@@ -294,10 +294,10 @@
|
|||||||
react-compiler-runtime "19.1.0-rc.1"
|
react-compiler-runtime "19.1.0-rc.1"
|
||||||
zustand "^5"
|
zustand "^5"
|
||||||
|
|
||||||
"@graphiql/plugin-explorer@4.0.6":
|
"@graphiql/plugin-explorer@3.2.6":
|
||||||
version "4.0.6"
|
version "3.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/@graphiql/plugin-explorer/-/plugin-explorer-4.0.6.tgz#bec1207dc27334914590ab31f46c2e944bbf4ebf"
|
resolved "https://registry.npmjs.org/@graphiql/plugin-explorer/-/plugin-explorer-3.2.6.tgz"
|
||||||
integrity sha512-TppIi92YPER3v70nlF01KTQrq9AiYqkZicSd1hpU7aqGmbqw/pLwBNLUEcfENBoJtw574Qxjswb01+GaYK0Tzw==
|
integrity sha512-MXzG/zVNzZfes4Em253bHyAbD/lwwAZkPKvxCAQkjz0i3dtcv4uF3D8iqJ7214iu3SCphbORYZZUC93fik1yew==
|
||||||
dependencies:
|
dependencies:
|
||||||
graphiql-explorer "^0.9.0"
|
graphiql-explorer "^0.9.0"
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-01-13 05:05+0000\n"
|
"POT-Creation-Date: 2026-01-08 05:04+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -1822,6 +1822,7 @@ msgid "ASN Count"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/circuits/tables/virtual_circuits.py:64
|
#: netbox/circuits/tables/virtual_circuits.py:64
|
||||||
|
#: netbox/netbox/navigation/menu.py:235
|
||||||
#: netbox/templates/circuits/virtualcircuit.html:87
|
#: netbox/templates/circuits/virtualcircuit.html:87
|
||||||
#: netbox/templates/vpn/l2vpn.html:60 netbox/templates/vpn/tunnel.html:72
|
#: netbox/templates/vpn/l2vpn.html:60 netbox/templates/vpn/tunnel.html:72
|
||||||
#: netbox/vpn/tables/tunnels.py:59
|
#: netbox/vpn/tables/tunnels.py:59
|
||||||
@@ -12189,10 +12190,6 @@ msgstr ""
|
|||||||
msgid "L2VPNs"
|
msgid "L2VPNs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/netbox/navigation/menu.py:235
|
|
||||||
msgid "L2VPN Terminations"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: netbox/netbox/navigation/menu.py:241
|
#: netbox/netbox/navigation/menu.py:241
|
||||||
msgid "IKE Proposals"
|
msgid "IKE Proposals"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -15941,7 +15938,7 @@ msgstr ""
|
|||||||
#: netbox/users/forms/model_forms.py:126
|
#: netbox/users/forms/model_forms.py:126
|
||||||
msgid ""
|
msgid ""
|
||||||
"Tokens must be at least 40 characters in length. <strong>Be sure to record "
|
"Tokens must be at least 40 characters in length. <strong>Be sure to record "
|
||||||
"your token</strong> prior to submitting this form, as it will no longer be "
|
"your key</strong> prior to submitting this form, as it will no longer be "
|
||||||
"accessible once the token has been created."
|
"accessible once the token has been created."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -16080,7 +16077,7 @@ msgid "write enabled"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/models/tokens.py:72
|
#: netbox/users/models/tokens.py:72
|
||||||
msgid "Permit create/update/delete operations using this token"
|
msgid "Permit create/update/delete operations using this key"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/models/tokens.py:76
|
#: netbox/users/models/tokens.py:76
|
||||||
@@ -16129,16 +16126,12 @@ msgstr ""
|
|||||||
msgid "tokens"
|
msgid "tokens"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/models/tokens.py:217
|
#: netbox/users/models/tokens.py:219
|
||||||
msgid "Unable to save v2 tokens: API_TOKEN_PEPPERS is not defined."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: netbox/users/models/tokens.py:222
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Invalid pepper ID: {id}. Check configured API_TOKEN_PEPPERS."
|
msgid "Invalid pepper ID: {id}. Check configured API_TOKEN_PEPPERS."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/users/models/tokens.py:235
|
#: netbox/users/models/tokens.py:232
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Expiration time must be in the future. Current server time is {current_time} "
|
"Expiration time must be in the future. Current server time is {current_time} "
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class UserTokenForm(forms.ModelForm):
|
|||||||
token = forms.CharField(
|
token = forms.CharField(
|
||||||
label=_('Token'),
|
label=_('Token'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
'Tokens must be at least 40 characters in length. <strong>Be sure to record your token</strong> prior to '
|
'Tokens must be at least 40 characters in length. <strong>Be sure to record your key</strong> prior to '
|
||||||
'submitting this form, as it will no longer be accessible once the token has been created.'
|
'submitting this form, as it will no longer be accessible once the token has been created.'
|
||||||
),
|
),
|
||||||
widget=forms.TextInput(
|
widget=forms.TextInput(
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class Token(models.Model):
|
|||||||
write_enabled = models.BooleanField(
|
write_enabled = models.BooleanField(
|
||||||
verbose_name=_('write enabled'),
|
verbose_name=_('write enabled'),
|
||||||
default=True,
|
default=True,
|
||||||
help_text=_('Permit create/update/delete operations using this token')
|
help_text=_('Permit create/update/delete operations using this key')
|
||||||
)
|
)
|
||||||
# For legacy v1 tokens, this field stores the plaintext 40-char token value. Not used for v2.
|
# For legacy v1 tokens, this field stores the plaintext 40-char token value. Not used for v2.
|
||||||
plaintext = models.CharField(
|
plaintext = models.CharField(
|
||||||
@@ -213,9 +213,6 @@ class Token(models.Model):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
if self.version == TokenVersionChoices.V2 and not settings.API_TOKEN_PEPPERS:
|
|
||||||
raise ValidationError(_("Unable to save v2 tokens: API_TOKEN_PEPPERS is not defined."))
|
|
||||||
|
|
||||||
if self._state.adding:
|
if self._state.adding:
|
||||||
if self.pepper_id is not None and self.pepper_id not in settings.API_TOKEN_PEPPERS:
|
if self.pepper_id is not None and self.pepper_id not in settings.API_TOKEN_PEPPERS:
|
||||||
raise ValidationError(_(
|
raise ValidationError(_(
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from users.choices import TokenVersionChoices
|
|
||||||
from users.models import User, Token
|
from users.models import User, Token
|
||||||
from utilities.testing import create_test_user
|
from utilities.testing import create_test_user
|
||||||
|
|
||||||
@@ -95,15 +94,6 @@ class TokenTest(TestCase):
|
|||||||
token.refresh_from_db()
|
token.refresh_from_db()
|
||||||
self.assertEqual(token.description, 'New Description')
|
self.assertEqual(token.description, 'New Description')
|
||||||
|
|
||||||
@override_settings(API_TOKEN_PEPPERS={})
|
|
||||||
def test_v2_without_peppers_configured(self):
|
|
||||||
"""
|
|
||||||
Attempting to save a v2 token without API_TOKEN_PEPPERS defined should raise a ValidationError.
|
|
||||||
"""
|
|
||||||
token = Token(version=TokenVersionChoices.V2)
|
|
||||||
with self.assertRaises(ValidationError):
|
|
||||||
token.clean()
|
|
||||||
|
|
||||||
|
|
||||||
class UserConfigTest(TestCase):
|
class UserConfigTest(TestCase):
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user