mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 00:36:11 -06:00
13132 add gettext_lazy to models
This commit is contained in:
parent
1195efc2d1
commit
8b4b331ffd
@ -3,6 +3,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from ..querysets import ObjectChangeQuerySet
|
from ..querysets import ObjectChangeQuerySet
|
||||||
@ -19,6 +20,7 @@ class ObjectChange(models.Model):
|
|||||||
parent device. This will ensure changes made to component models appear in the parent model's changelog.
|
parent device. This will ensure changes made to component models appear in the parent model's changelog.
|
||||||
"""
|
"""
|
||||||
time = models.DateTimeField(
|
time = models.DateTimeField(
|
||||||
|
verbose_name=_('time'),
|
||||||
auto_now_add=True,
|
auto_now_add=True,
|
||||||
editable=False,
|
editable=False,
|
||||||
db_index=True
|
db_index=True
|
||||||
@ -31,14 +33,17 @@ class ObjectChange(models.Model):
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
user_name = models.CharField(
|
user_name = models.CharField(
|
||||||
|
verbose_name=_('user name'),
|
||||||
max_length=150,
|
max_length=150,
|
||||||
editable=False
|
editable=False
|
||||||
)
|
)
|
||||||
request_id = models.UUIDField(
|
request_id = models.UUIDField(
|
||||||
|
verbose_name=_('request id'),
|
||||||
editable=False,
|
editable=False,
|
||||||
db_index=True
|
db_index=True
|
||||||
)
|
)
|
||||||
action = models.CharField(
|
action = models.CharField(
|
||||||
|
verbose_name=_('action'),
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=ObjectChangeActionChoices
|
choices=ObjectChangeActionChoices
|
||||||
)
|
)
|
||||||
@ -72,11 +77,13 @@ class ObjectChange(models.Model):
|
|||||||
editable=False
|
editable=False
|
||||||
)
|
)
|
||||||
prechange_data = models.JSONField(
|
prechange_data = models.JSONField(
|
||||||
|
verbose_name=_('prechange data'),
|
||||||
editable=False,
|
editable=False,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
postchange_data = models.JSONField(
|
postchange_data = models.JSONField(
|
||||||
|
verbose_name=_('postchange data'),
|
||||||
editable=False,
|
editable=False,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
|
@ -2,7 +2,7 @@ from django.conf import settings
|
|||||||
from django.core.validators import ValidationError
|
from django.core.validators import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from jinja2.loaders import BaseLoader
|
from jinja2.loaders import BaseLoader
|
||||||
from jinja2.sandbox import SandboxedEnvironment
|
from jinja2.sandbox import SandboxedEnvironment
|
||||||
|
|
||||||
@ -31,17 +31,21 @@ class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel):
|
|||||||
will be available to a Device in site A assigned to tenant B. Data is stored in JSON format.
|
will be available to a Device in site A assigned to tenant B. Data is stored in JSON format.
|
||||||
"""
|
"""
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
weight = models.PositiveSmallIntegerField(
|
weight = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name=_('weight'),
|
||||||
default=1000
|
default=1000
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
|
verbose_name=_('description'),
|
||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
is_active = models.BooleanField(
|
is_active = models.BooleanField(
|
||||||
|
verbose_name=_('is active'),
|
||||||
default=True,
|
default=True,
|
||||||
)
|
)
|
||||||
regions = models.ManyToManyField(
|
regions = models.ManyToManyField(
|
||||||
@ -138,7 +142,7 @@ class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel):
|
|||||||
# Verify that JSON data is provided as an object
|
# Verify that JSON data is provided as an object
|
||||||
if type(self.data) is not dict:
|
if type(self.data) is not dict:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{'data': 'JSON data must be in object form. Example: {"foo": 123}'}
|
{'data': _('JSON data must be in object form. Example: {"foo": 123}')}
|
||||||
)
|
)
|
||||||
|
|
||||||
def sync_data(self):
|
def sync_data(self):
|
||||||
@ -194,7 +198,7 @@ class ConfigContextModel(models.Model):
|
|||||||
# Verify that JSON data is provided as an object
|
# Verify that JSON data is provided as an object
|
||||||
if self.local_context_data and type(self.local_context_data) is not dict:
|
if self.local_context_data and type(self.local_context_data) is not dict:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{'local_context_data': 'JSON data must be in object form. Example: {"foo": 123}'}
|
{'local_context_data': _('JSON data must be in object form. Example: {"foo": 123}')}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -204,16 +208,20 @@ class ConfigContextModel(models.Model):
|
|||||||
|
|
||||||
class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
|
verbose_name=_('name'),
|
||||||
max_length=100
|
max_length=100
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
|
verbose_name=_('description'),
|
||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
template_code = models.TextField(
|
template_code = models.TextField(
|
||||||
|
verbose_name=_('template code'),
|
||||||
help_text=_('Jinja2 template code.')
|
help_text=_('Jinja2 template code.')
|
||||||
)
|
)
|
||||||
environment_params = models.JSONField(
|
environment_params = models.JSONField(
|
||||||
|
verbose_name=_('environment params'),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
default=dict,
|
default=dict,
|
||||||
|
@ -12,7 +12,7 @@ from django.db import models
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
@ -64,6 +64,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
help_text=_('The object(s) to which this field applies.')
|
help_text=_('The object(s) to which this field applies.')
|
||||||
)
|
)
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
|
verbose_name=_('type'),
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=CustomFieldTypeChoices,
|
choices=CustomFieldTypeChoices,
|
||||||
default=CustomFieldTypeChoices.TYPE_TEXT,
|
default=CustomFieldTypeChoices.TYPE_TEXT,
|
||||||
@ -77,49 +78,56 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
help_text=_('The type of NetBox object this field maps to (for object fields)')
|
help_text=_('The type of NetBox object this field maps to (for object fields)')
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
|
verbose_name=_('name'),
|
||||||
max_length=50,
|
max_length=50,
|
||||||
unique=True,
|
unique=True,
|
||||||
help_text=_('Internal field name'),
|
help_text=_('Internal field name'),
|
||||||
validators=(
|
validators=(
|
||||||
RegexValidator(
|
RegexValidator(
|
||||||
regex=r'^[a-z0-9_]+$',
|
regex=r'^[a-z0-9_]+$',
|
||||||
message="Only alphanumeric characters and underscores are allowed.",
|
message=_("Only alphanumeric characters and underscores are allowed."),
|
||||||
flags=re.IGNORECASE
|
flags=re.IGNORECASE
|
||||||
),
|
),
|
||||||
RegexValidator(
|
RegexValidator(
|
||||||
regex=r'__',
|
regex=r'__',
|
||||||
message="Double underscores are not permitted in custom field names.",
|
message=_("Double underscores are not permitted in custom field names."),
|
||||||
flags=re.IGNORECASE,
|
flags=re.IGNORECASE,
|
||||||
inverse_match=True
|
inverse_match=True
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
label = models.CharField(
|
label = models.CharField(
|
||||||
|
verbose_name=_('label'),
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_('Name of the field as displayed to users (if not provided, '
|
help_text=_('Name of the field as displayed to users (if not provided, '
|
||||||
'the field\'s name will be used)')
|
'the field\'s name will be used)')
|
||||||
)
|
)
|
||||||
group_name = models.CharField(
|
group_name = models.CharField(
|
||||||
|
verbose_name=_('group name'),
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_("Custom fields within the same group will be displayed together")
|
help_text=_("Custom fields within the same group will be displayed together")
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
|
verbose_name=_('description'),
|
||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
required = models.BooleanField(
|
required = models.BooleanField(
|
||||||
|
verbose_name=_('required'),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_('If true, this field is required when creating new objects '
|
help_text=_('If true, this field is required when creating new objects '
|
||||||
'or editing an existing object.')
|
'or editing an existing object.')
|
||||||
)
|
)
|
||||||
search_weight = models.PositiveSmallIntegerField(
|
search_weight = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name=_('search weight'),
|
||||||
default=1000,
|
default=1000,
|
||||||
help_text=_('Weighting for search. Lower values are considered more important. '
|
help_text=_('Weighting for search. Lower values are considered more important. '
|
||||||
'Fields with a search weight of zero will be ignored.')
|
'Fields with a search weight of zero will be ignored.')
|
||||||
)
|
)
|
||||||
filter_logic = models.CharField(
|
filter_logic = models.CharField(
|
||||||
|
verbose_name=_('filter logic'),
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=CustomFieldFilterLogicChoices,
|
choices=CustomFieldFilterLogicChoices,
|
||||||
default=CustomFieldFilterLogicChoices.FILTER_LOOSE,
|
default=CustomFieldFilterLogicChoices.FILTER_LOOSE,
|
||||||
@ -127,6 +135,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
'matches the entire field.')
|
'matches the entire field.')
|
||||||
)
|
)
|
||||||
default = models.JSONField(
|
default = models.JSONField(
|
||||||
|
verbose_name=_('default'),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text=_('Default value for the field (must be a JSON value). Encapsulate '
|
help_text=_('Default value for the field (must be a JSON value). Encapsulate '
|
||||||
@ -134,35 +143,41 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
)
|
)
|
||||||
weight = models.PositiveSmallIntegerField(
|
weight = models.PositiveSmallIntegerField(
|
||||||
default=100,
|
default=100,
|
||||||
verbose_name='Display weight',
|
verbose_name=_('Display weight'),
|
||||||
help_text=_('Fields with higher weights appear lower in a form.')
|
help_text=_('Fields with higher weights appear lower in a form.')
|
||||||
)
|
)
|
||||||
validation_minimum = models.IntegerField(
|
validation_minimum = models.IntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name='Minimum value',
|
verbose_name=_('Minimum value'),
|
||||||
help_text=_('Minimum allowed value (for numeric fields)')
|
help_text=_('Minimum allowed value (for numeric fields)')
|
||||||
)
|
)
|
||||||
validation_maximum = models.IntegerField(
|
validation_maximum = models.IntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name='Maximum value',
|
verbose_name=_('Maximum value'),
|
||||||
help_text=_('Maximum allowed value (for numeric fields)')
|
help_text=_('Maximum allowed value (for numeric fields)')
|
||||||
)
|
)
|
||||||
validation_regex = models.CharField(
|
validation_regex = models.CharField(
|
||||||
blank=True,
|
blank=True,
|
||||||
validators=[validate_regex],
|
validators=[validate_regex],
|
||||||
max_length=500,
|
max_length=500,
|
||||||
verbose_name='Validation regex',
|
verbose_name=_('Validation regex'),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
'Regular expression to enforce on text field values. Use ^ and $ to force matching of entire string. For '
|
'Regular expression to enforce on text field values. Use ^ and $ to force matching of entire string. For '
|
||||||
'example, <code>^[A-Z]{3}$</code> will limit values to exactly three uppercase letters.'
|
'example, <code>^[A-Z]{3}$</code> will limit values to exactly three uppercase letters.'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
<<<<<<< HEAD
|
||||||
choice_set = models.ForeignKey(
|
choice_set = models.ForeignKey(
|
||||||
to='CustomFieldChoiceSet',
|
to='CustomFieldChoiceSet',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='choices_for',
|
related_name='choices_for',
|
||||||
|
=======
|
||||||
|
choices = ArrayField(
|
||||||
|
verbose_name=_('choices'),
|
||||||
|
base_field=models.CharField(max_length=100),
|
||||||
|
>>>>>>> 9a447ed8f (13132 add gettext_lazy to models)
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
@ -170,12 +185,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
max_length=50,
|
max_length=50,
|
||||||
choices=CustomFieldVisibilityChoices,
|
choices=CustomFieldVisibilityChoices,
|
||||||
default=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
|
default=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE,
|
||||||
verbose_name='UI visibility',
|
verbose_name=_('UI visibility'),
|
||||||
help_text=_('Specifies the visibility of custom field in the UI')
|
help_text=_('Specifies the visibility of custom field in the UI')
|
||||||
)
|
)
|
||||||
is_cloneable = models.BooleanField(
|
is_cloneable = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name='Cloneable',
|
verbose_name=_('Cloneable'),
|
||||||
help_text=_('Replicate this value when cloning objects')
|
help_text=_('Replicate this value when cloning objects')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -265,15 +280,15 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
self.validate(default_value)
|
self.validate(default_value)
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'default': f'Invalid default value "{self.default}": {err.message}'
|
'default': _('Invalid default value "{default}": {message}').format(default=self.default, message=self.message)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Minimum/maximum values can be set only for numeric fields
|
# Minimum/maximum values can be set only for numeric fields
|
||||||
if self.type not in (CustomFieldTypeChoices.TYPE_INTEGER, CustomFieldTypeChoices.TYPE_DECIMAL):
|
if self.type not in (CustomFieldTypeChoices.TYPE_INTEGER, CustomFieldTypeChoices.TYPE_DECIMAL):
|
||||||
if self.validation_minimum:
|
if self.validation_minimum:
|
||||||
raise ValidationError({'validation_minimum': "A minimum value may be set only for numeric fields"})
|
raise ValidationError({'validation_minimum': _("A minimum value may be set only for numeric fields")})
|
||||||
if self.validation_maximum:
|
if self.validation_maximum:
|
||||||
raise ValidationError({'validation_maximum': "A maximum value may be set only for numeric fields"})
|
raise ValidationError({'validation_maximum': _("A maximum value may be set only for numeric fields")})
|
||||||
|
|
||||||
# Regex validation can be set only for text fields
|
# Regex validation can be set only for text fields
|
||||||
regex_types = (
|
regex_types = (
|
||||||
@ -283,10 +298,23 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
)
|
)
|
||||||
if self.validation_regex and self.type not in regex_types:
|
if self.validation_regex and self.type not in regex_types:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'validation_regex': "Regular expression validation is supported only for text and URL fields"
|
'validation_regex': _("Regular expression validation is supported only for text and URL fields")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
# Choice set must be set on selection fields, and *only* on selection fields
|
# Choice set must be set on selection fields, and *only* on selection fields
|
||||||
|
=======
|
||||||
|
# Choices can be set only on selection fields
|
||||||
|
if self.choices and self.type not in (
|
||||||
|
CustomFieldTypeChoices.TYPE_SELECT,
|
||||||
|
CustomFieldTypeChoices.TYPE_MULTISELECT
|
||||||
|
):
|
||||||
|
raise ValidationError({
|
||||||
|
'choices': _("Choices may be set only for custom selection fields.")
|
||||||
|
})
|
||||||
|
|
||||||
|
# Selection fields must have at least one choice defined
|
||||||
|
>>>>>>> 9a447ed8f (13132 add gettext_lazy to models)
|
||||||
if self.type in (
|
if self.type in (
|
||||||
CustomFieldTypeChoices.TYPE_SELECT,
|
CustomFieldTypeChoices.TYPE_SELECT,
|
||||||
CustomFieldTypeChoices.TYPE_MULTISELECT
|
CustomFieldTypeChoices.TYPE_MULTISELECT
|
||||||
@ -297,24 +325,28 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
})
|
})
|
||||||
elif self.choice_set:
|
elif self.choice_set:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
|
<<<<<<< HEAD
|
||||||
'choice_set': "Choices may be set only on selection fields."
|
'choice_set': "Choices may be set only on selection fields."
|
||||||
|
=======
|
||||||
|
'choices': _("Selection fields must specify at least one choice.")
|
||||||
|
>>>>>>> 9a447ed8f (13132 add gettext_lazy to models)
|
||||||
})
|
})
|
||||||
|
|
||||||
# A selection field's default (if any) must be present in its available choices
|
# A selection field's default (if any) must be present in its available choices
|
||||||
if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.default and self.default not in self.choices:
|
if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.default and self.default not in self.choices:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'default': f"The specified default value ({self.default}) is not listed as an available choice."
|
'default': _("The specified default value ({default}) is not listed as an available choice.").format(default=self.default)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Object fields must define an object_type; other fields must not
|
# Object fields must define an object_type; other fields must not
|
||||||
if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
|
if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
|
||||||
if not self.object_type:
|
if not self.object_type:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'object_type': "Object fields must define an object type."
|
'object_type': _("Object fields must define an object type.")
|
||||||
})
|
})
|
||||||
elif self.object_type:
|
elif self.object_type:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'object_type': f"{self.get_type_display()} fields may not define an object type."
|
'object_type': _("{type_display} fields may not define an object type.".format(type_display=self.get_type_display()))
|
||||||
})
|
})
|
||||||
|
|
||||||
def serialize(self, value):
|
def serialize(self, value):
|
||||||
@ -393,8 +425,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN:
|
||||||
choices = (
|
choices = (
|
||||||
(None, '---------'),
|
(None, '---------'),
|
||||||
(True, 'True'),
|
(True, _('True')),
|
||||||
(False, 'False'),
|
(False, _('False')),
|
||||||
)
|
)
|
||||||
field = forms.NullBooleanField(
|
field = forms.NullBooleanField(
|
||||||
required=required, initial=initial, widget=forms.Select(choices=choices)
|
required=required, initial=initial, widget=forms.Select(choices=choices)
|
||||||
@ -463,7 +495,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
field.validators = [
|
field.validators = [
|
||||||
RegexValidator(
|
RegexValidator(
|
||||||
regex=self.validation_regex,
|
regex=self.validation_regex,
|
||||||
message=mark_safe(f"Values must match this regex: <code>{self.validation_regex}</code>")
|
message=mark_safe(_("Values must match this regex: <code>{regex}</code>").format(regex=self.validation_regex))
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -476,7 +508,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
if enforce_visibility and self.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
|
if enforce_visibility and self.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY:
|
||||||
field.disabled = True
|
field.disabled = True
|
||||||
prepend = '<br />' if field.help_text else ''
|
prepend = '<br />' if field.help_text else ''
|
||||||
field.help_text += f'{prepend}<i class="mdi mdi-alert-circle-outline"></i> Field is set to read-only.'
|
field.help_text += f'{prepend}<i class="mdi mdi-alert-circle-outline"></i> ' + _('Field is set to read-only.')
|
||||||
|
|
||||||
return field
|
return field
|
||||||
|
|
||||||
@ -558,33 +590,33 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
# Validate text field
|
# Validate text field
|
||||||
if self.type in (CustomFieldTypeChoices.TYPE_TEXT, CustomFieldTypeChoices.TYPE_LONGTEXT):
|
if self.type in (CustomFieldTypeChoices.TYPE_TEXT, CustomFieldTypeChoices.TYPE_LONGTEXT):
|
||||||
if type(value) is not str:
|
if type(value) is not str:
|
||||||
raise ValidationError(f"Value must be a string.")
|
raise ValidationError(_("Value must be a string."))
|
||||||
if self.validation_regex and not re.match(self.validation_regex, value):
|
if self.validation_regex and not re.match(self.validation_regex, value):
|
||||||
raise ValidationError(f"Value must match regex '{self.validation_regex}'")
|
raise ValidationError(_("Value must match regex '{regex}'").format(regex=self.validation_regex))
|
||||||
|
|
||||||
# Validate integer
|
# Validate integer
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
|
||||||
if type(value) is not int:
|
if type(value) is not int:
|
||||||
raise ValidationError("Value must be an integer.")
|
raise ValidationError(_("Value must be an integer."))
|
||||||
if self.validation_minimum is not None and value < self.validation_minimum:
|
if self.validation_minimum is not None and value < self.validation_minimum:
|
||||||
raise ValidationError(f"Value must be at least {self.validation_minimum}")
|
raise ValidationError(_("Value must be at least {validation_minimum}").format(validation_minimum=self.validation_maximum))
|
||||||
if self.validation_maximum is not None and value > self.validation_maximum:
|
if self.validation_maximum is not None and value > self.validation_maximum:
|
||||||
raise ValidationError(f"Value must not exceed {self.validation_maximum}")
|
raise ValidationError(_("Value must not exceed {validation_maximum}").format(validation_maximum=self.validation_maximum))
|
||||||
|
|
||||||
# Validate decimal
|
# Validate decimal
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
|
elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL:
|
||||||
try:
|
try:
|
||||||
decimal.Decimal(value)
|
decimal.Decimal(value)
|
||||||
except decimal.InvalidOperation:
|
except decimal.InvalidOperation:
|
||||||
raise ValidationError("Value must be a decimal.")
|
raise ValidationError(_("Value must be a decimal."))
|
||||||
if self.validation_minimum is not None and value < self.validation_minimum:
|
if self.validation_minimum is not None and value < self.validation_minimum:
|
||||||
raise ValidationError(f"Value must be at least {self.validation_minimum}")
|
raise ValidationError(_("Value must be at least {validation_minimum}").format(validation_minimum=self.validation_minimum))
|
||||||
if self.validation_maximum is not None and value > self.validation_maximum:
|
if self.validation_maximum is not None and value > self.validation_maximum:
|
||||||
raise ValidationError(f"Value must not exceed {self.validation_maximum}")
|
raise ValidationError(_("Value must not exceed {validation_maximum}").format(validation_maximum=self.validation_maximum))
|
||||||
|
|
||||||
# Validate boolean
|
# Validate boolean
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN and value not in [True, False, 1, 0]:
|
||||||
raise ValidationError("Value must be true or false.")
|
raise ValidationError(_("Value must be true or false."))
|
||||||
|
|
||||||
# Validate date
|
# Validate date
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_DATE:
|
elif self.type == CustomFieldTypeChoices.TYPE_DATE:
|
||||||
@ -592,7 +624,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
try:
|
try:
|
||||||
date.fromisoformat(value)
|
date.fromisoformat(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValidationError("Date values must be in ISO 8601 format (YYYY-MM-DD).")
|
raise ValidationError(_("Date values must be in ISO 8601 format (YYYY-MM-DD)."))
|
||||||
|
|
||||||
# Validate date & time
|
# Validate date & time
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_DATETIME:
|
elif self.type == CustomFieldTypeChoices.TYPE_DATETIME:
|
||||||
@ -600,37 +632,38 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
try:
|
try:
|
||||||
datetime.fromisoformat(value)
|
datetime.fromisoformat(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValidationError("Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS).")
|
raise ValidationError(_("Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS)."))
|
||||||
|
|
||||||
# Validate selected choice
|
# Validate selected choice
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
elif self.type == CustomFieldTypeChoices.TYPE_SELECT:
|
||||||
if value not in self.choices:
|
if value not in self.choices:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Invalid choice ({value}). Available choices are: {', '.join(self.choices)}"
|
_("Invalid choice ({value}). Available choices are: {choices}").format(value=value, choices=', '.join(self.choices))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate all selected choices
|
# Validate all selected choices
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
||||||
if not set(value).issubset(self.choices):
|
if not set(value).issubset(self.choices):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Invalid choice(s) ({', '.join(value)}). Available choices are: {', '.join(self.choices)}"
|
_("Invalid choice(s) ({invalid_choices}). Available choices are: {available_choices}").format(
|
||||||
|
invalid_choices=', '.join(value), available_choices=', '.join(self.choices))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate selected object
|
# Validate selected object
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
|
||||||
if type(value) is not int:
|
if type(value) is not int:
|
||||||
raise ValidationError(f"Value must be an object ID, not {type(value).__name__}")
|
raise ValidationError(_("Value must be an object ID, not {type}").format(type=type(value).__name__))
|
||||||
|
|
||||||
# Validate selected objects
|
# Validate selected objects
|
||||||
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
|
||||||
if type(value) is not list:
|
if type(value) is not list:
|
||||||
raise ValidationError(f"Value must be a list of object IDs, not {type(value).__name__}")
|
raise ValidationError(_("Value must be a list of object IDs, not {type}").format(type=type(value).__name__))
|
||||||
for id in value:
|
for id in value:
|
||||||
if type(id) is not int:
|
if type(id) is not int:
|
||||||
raise ValidationError(f"Found invalid object ID: {id}")
|
raise ValidationError(_("Found invalid object ID: {id}").format(id=id))
|
||||||
|
|
||||||
elif self.required:
|
elif self.required:
|
||||||
raise ValidationError("Required field cannot be empty.")
|
raise ValidationError(_("Required field cannot be empty."))
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||||
@ -680,3 +713,4 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
|
|||||||
self.extra_choices = sorted(self.choices)
|
self.extra_choices = sorted(self.choices)
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from extras.dashboard.utils import get_widget_class
|
from extras.dashboard.utils import get_widget_class
|
||||||
|
|
||||||
@ -15,9 +16,11 @@ class Dashboard(models.Model):
|
|||||||
related_name='dashboard'
|
related_name='dashboard'
|
||||||
)
|
)
|
||||||
layout = models.JSONField(
|
layout = models.JSONField(
|
||||||
|
verbose_name=_('layout'),
|
||||||
default=list
|
default=list
|
||||||
)
|
)
|
||||||
config = models.JSONField(
|
config = models.JSONField(
|
||||||
|
verbose_name=_('config'),
|
||||||
default=dict
|
default=dict
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ from django.http import HttpResponse
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.utils.encoders import JSONEncoder
|
from rest_framework.utils.encoders import JSONEncoder
|
||||||
|
|
||||||
from extras.choices import *
|
from extras.choices import *
|
||||||
@ -53,64 +53,74 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
help_text=_("The object(s) to which this Webhook applies.")
|
help_text=_("The object(s) to which this Webhook applies.")
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
|
verbose_name=_('name'),
|
||||||
max_length=150,
|
max_length=150,
|
||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
type_create = models.BooleanField(
|
type_create = models.BooleanField(
|
||||||
|
verbose_name=_('type create'),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Triggers when a matching object is created.")
|
help_text=_("Triggers when a matching object is created.")
|
||||||
)
|
)
|
||||||
type_update = models.BooleanField(
|
type_update = models.BooleanField(
|
||||||
|
verbose_name=_('type update'),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Triggers when a matching object is updated.")
|
help_text=_("Triggers when a matching object is updated.")
|
||||||
)
|
)
|
||||||
type_delete = models.BooleanField(
|
type_delete = models.BooleanField(
|
||||||
|
verbose_name=_('type delete'),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Triggers when a matching object is deleted.")
|
help_text=_("Triggers when a matching object is deleted.")
|
||||||
)
|
)
|
||||||
type_job_start = models.BooleanField(
|
type_job_start = models.BooleanField(
|
||||||
|
verbose_name=_('type job start'),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Triggers when a job for a matching object is started.")
|
help_text=_("Triggers when a job for a matching object is started.")
|
||||||
)
|
)
|
||||||
type_job_end = models.BooleanField(
|
type_job_end = models.BooleanField(
|
||||||
|
verbose_name=_('type job end'),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Triggers when a job for a matching object terminates.")
|
help_text=_("Triggers when a job for a matching object terminates.")
|
||||||
)
|
)
|
||||||
payload_url = models.CharField(
|
payload_url = models.CharField(
|
||||||
max_length=500,
|
max_length=500,
|
||||||
verbose_name='URL',
|
verbose_name=_('URL'),
|
||||||
help_text=_('This URL will be called using the HTTP method defined when the webhook is called. '
|
help_text=_('This URL will be called using the HTTP method defined when the webhook is called. '
|
||||||
'Jinja2 template processing is supported with the same context as the request body.')
|
'Jinja2 template processing is supported with the same context as the request body.')
|
||||||
)
|
)
|
||||||
enabled = models.BooleanField(
|
enabled = models.BooleanField(
|
||||||
|
verbose_name=_('enabled'),
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
http_method = models.CharField(
|
http_method = models.CharField(
|
||||||
max_length=30,
|
max_length=30,
|
||||||
choices=WebhookHttpMethodChoices,
|
choices=WebhookHttpMethodChoices,
|
||||||
default=WebhookHttpMethodChoices.METHOD_POST,
|
default=WebhookHttpMethodChoices.METHOD_POST,
|
||||||
verbose_name='HTTP method'
|
verbose_name=_('HTTP method')
|
||||||
)
|
)
|
||||||
http_content_type = models.CharField(
|
http_content_type = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
default=HTTP_CONTENT_TYPE_JSON,
|
default=HTTP_CONTENT_TYPE_JSON,
|
||||||
verbose_name='HTTP content type',
|
verbose_name=_('HTTP content type'),
|
||||||
help_text=_('The complete list of official content types is available '
|
help_text=_('The complete list of official content types is available '
|
||||||
'<a href="https://www.iana.org/assignments/media-types/media-types.xhtml">here</a>.')
|
'<a href="https://www.iana.org/assignments/media-types/media-types.xhtml">here</a>.')
|
||||||
)
|
)
|
||||||
additional_headers = models.TextField(
|
additional_headers = models.TextField(
|
||||||
|
verbose_name=_('additional headers'),
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_("User-supplied HTTP headers to be sent with the request in addition to the HTTP content type. "
|
help_text=_("User-supplied HTTP headers to be sent with the request in addition to the HTTP content type. "
|
||||||
"Headers should be defined in the format <code>Name: Value</code>. Jinja2 template processing is "
|
"Headers should be defined in the format <code>Name: Value</code>. Jinja2 template processing is "
|
||||||
"supported with the same context as the request body (below).")
|
"supported with the same context as the request body (below).")
|
||||||
)
|
)
|
||||||
body_template = models.TextField(
|
body_template = models.TextField(
|
||||||
|
verbose_name=_('body template'),
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_('Jinja2 template for a custom request body. If blank, a JSON object representing the change will be '
|
help_text=_('Jinja2 template for a custom request body. If blank, a JSON object representing the change will be '
|
||||||
'included. Available context data includes: <code>event</code>, <code>model</code>, '
|
'included. Available context data includes: <code>event</code>, <code>model</code>, '
|
||||||
'<code>timestamp</code>, <code>username</code>, <code>request_id</code>, and <code>data</code>.')
|
'<code>timestamp</code>, <code>username</code>, <code>request_id</code>, and <code>data</code>.')
|
||||||
)
|
)
|
||||||
secret = models.CharField(
|
secret = models.CharField(
|
||||||
|
verbose_name=_('secret'),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_("When provided, the request will include a 'X-Hook-Signature' "
|
help_text=_("When provided, the request will include a 'X-Hook-Signature' "
|
||||||
@ -119,20 +129,21 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
"the request.")
|
"the request.")
|
||||||
)
|
)
|
||||||
conditions = models.JSONField(
|
conditions = models.JSONField(
|
||||||
|
verbose_name=_('conditions'),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text=_("A set of conditions which determine whether the webhook will be generated.")
|
help_text=_("A set of conditions which determine whether the webhook will be generated.")
|
||||||
)
|
)
|
||||||
ssl_verification = models.BooleanField(
|
ssl_verification = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
verbose_name='SSL verification',
|
verbose_name=_('SSL verification'),
|
||||||
help_text=_("Enable SSL certificate verification. Disable with caution!")
|
help_text=_("Enable SSL certificate verification. Disable with caution!")
|
||||||
)
|
)
|
||||||
ca_file_path = models.CharField(
|
ca_file_path = models.CharField(
|
||||||
max_length=4096,
|
max_length=4096,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name='CA File Path',
|
verbose_name=_('CA File Path'),
|
||||||
help_text=_('The specific CA certificate file to use for SSL verification. '
|
help_text=_('The specific CA certificate file to use for SSL verification. '
|
||||||
'Leave blank to use the system defaults.')
|
'Leave blank to use the system defaults.')
|
||||||
)
|
)
|
||||||
@ -164,7 +175,7 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
self.type_create, self.type_update, self.type_delete, self.type_job_start, self.type_job_end
|
self.type_create, self.type_update, self.type_delete, self.type_job_start, self.type_job_end
|
||||||
]):
|
]):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"At least one event type must be selected: create, update, delete, job_start, and/or job_end."
|
_("At least one event type must be selected: create, update, delete, job_start, and/or job_end.")
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.conditions:
|
if self.conditions:
|
||||||
@ -176,7 +187,7 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
# CA file path requires SSL verification enabled
|
# CA file path requires SSL verification enabled
|
||||||
if not self.ssl_verification and self.ca_file_path:
|
if not self.ssl_verification and self.ca_file_path:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'ca_file_path': 'Do not specify a CA certificate file if SSL verification is disabled.'
|
'ca_file_path': _('Do not specify a CA certificate file if SSL verification is disabled.')
|
||||||
})
|
})
|
||||||
|
|
||||||
def render_headers(self, context):
|
def render_headers(self, context):
|
||||||
@ -219,34 +230,41 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
help_text=_('The object type(s) to which this link applies.')
|
help_text=_('The object type(s) to which this link applies.')
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
enabled = models.BooleanField(
|
enabled = models.BooleanField(
|
||||||
|
verbose_name=_('enabled'),
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
link_text = models.TextField(
|
link_text = models.TextField(
|
||||||
|
verbose_name=_('link text'),
|
||||||
help_text=_("Jinja2 template code for link text")
|
help_text=_("Jinja2 template code for link text")
|
||||||
)
|
)
|
||||||
link_url = models.TextField(
|
link_url = models.TextField(
|
||||||
verbose_name='Link URL',
|
verbose_name=_('Link URL'),
|
||||||
help_text=_("Jinja2 template code for link URL")
|
help_text=_("Jinja2 template code for link URL")
|
||||||
)
|
)
|
||||||
weight = models.PositiveSmallIntegerField(
|
weight = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name=_('weight'),
|
||||||
default=100
|
default=100
|
||||||
)
|
)
|
||||||
group_name = models.CharField(
|
group_name = models.CharField(
|
||||||
|
verbose_name=_('group name'),
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_("Links with the same group will appear as a dropdown menu")
|
help_text=_("Links with the same group will appear as a dropdown menu")
|
||||||
)
|
)
|
||||||
button_class = models.CharField(
|
button_class = models.CharField(
|
||||||
|
verbose_name=_('button class'),
|
||||||
max_length=30,
|
max_length=30,
|
||||||
choices=CustomLinkButtonClassChoices,
|
choices=CustomLinkButtonClassChoices,
|
||||||
default=CustomLinkButtonClassChoices.DEFAULT,
|
default=CustomLinkButtonClassChoices.DEFAULT,
|
||||||
help_text=_("The class of the first link in a group will be used for the dropdown button")
|
help_text=_("The class of the first link in a group will be used for the dropdown button")
|
||||||
)
|
)
|
||||||
new_window = models.BooleanField(
|
new_window = models.BooleanField(
|
||||||
|
verbose_name=_('new window'),
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Force link to open in a new window")
|
help_text=_("Force link to open in a new window")
|
||||||
)
|
)
|
||||||
@ -306,9 +324,11 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
|
|||||||
help_text=_('The object type(s) to which this template applies.')
|
help_text=_('The object type(s) to which this template applies.')
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
|
verbose_name=_('name'),
|
||||||
max_length=100
|
max_length=100
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
|
verbose_name=_('description'),
|
||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
@ -319,15 +339,17 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
|
|||||||
mime_type = models.CharField(
|
mime_type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name='MIME type',
|
verbose_name=_('MIME type'),
|
||||||
help_text=_('Defaults to <code>text/plain; charset=utf-8</code>')
|
help_text=_('Defaults to <code>text/plain; charset=utf-8</code>')
|
||||||
)
|
)
|
||||||
file_extension = models.CharField(
|
file_extension = models.CharField(
|
||||||
|
verbose_name=_('file extension'),
|
||||||
max_length=15,
|
max_length=15,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_('Extension to append to the rendered filename')
|
help_text=_('Extension to append to the rendered filename')
|
||||||
)
|
)
|
||||||
as_attachment = models.BooleanField(
|
as_attachment = models.BooleanField(
|
||||||
|
verbose_name=_('as attachment'),
|
||||||
default=True,
|
default=True,
|
||||||
help_text=_("Download file as attachment")
|
help_text=_("Download file as attachment")
|
||||||
)
|
)
|
||||||
@ -354,7 +376,7 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
|
|||||||
|
|
||||||
if self.name.lower() == 'table':
|
if self.name.lower() == 'table':
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'name': f'"{self.name}" is a reserved name. Please choose a different name.'
|
'name': _('"{name}" is a reserved name. Please choose a different name.').format(name=self.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
def sync_data(self):
|
def sync_data(self):
|
||||||
@ -407,14 +429,17 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
help_text=_('The object type(s) to which this filter applies.')
|
help_text=_('The object type(s) to which this filter applies.')
|
||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
|
verbose_name=_('slug'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
|
verbose_name=_('description'),
|
||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
@ -425,15 +450,20 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
weight = models.PositiveSmallIntegerField(
|
weight = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name=_('weight'),
|
||||||
default=100
|
default=100
|
||||||
)
|
)
|
||||||
enabled = models.BooleanField(
|
enabled = models.BooleanField(
|
||||||
|
verbose_name=_('enabled'),
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
shared = models.BooleanField(
|
shared = models.BooleanField(
|
||||||
|
verbose_name=_('shared'),
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
parameters = models.JSONField()
|
parameters = models.JSONField(
|
||||||
|
verbose_name=_('parameters')
|
||||||
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'content_types', 'weight', 'enabled', 'parameters',
|
'content_types', 'weight', 'enabled', 'parameters',
|
||||||
@ -458,7 +488,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
|||||||
# Verify that `parameters` is a JSON object
|
# Verify that `parameters` is a JSON object
|
||||||
if type(self.parameters) is not dict:
|
if type(self.parameters) is not dict:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{'parameters': 'Filter parameters must be stored as a dictionary of keyword arguments.'}
|
{'parameters': _('Filter parameters must be stored as a dictionary of keyword arguments.')}
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -485,9 +515,14 @@ class ImageAttachment(ChangeLoggedModel):
|
|||||||
height_field='image_height',
|
height_field='image_height',
|
||||||
width_field='image_width'
|
width_field='image_width'
|
||||||
)
|
)
|
||||||
image_height = models.PositiveSmallIntegerField()
|
image_height = models.PositiveSmallIntegerField(
|
||||||
image_width = models.PositiveSmallIntegerField()
|
verbose_name=_('image height'),
|
||||||
|
)
|
||||||
|
image_width = models.PositiveSmallIntegerField(
|
||||||
|
verbose_name=_('image width'),
|
||||||
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
|
verbose_name=_('name'),
|
||||||
max_length=50,
|
max_length=50,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
@ -565,11 +600,14 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
kind = models.CharField(
|
kind = models.CharField(
|
||||||
|
verbose_name=_('kind'),
|
||||||
max_length=30,
|
max_length=30,
|
||||||
choices=JournalEntryKindChoices,
|
choices=JournalEntryKindChoices,
|
||||||
default=JournalEntryKindChoices.KIND_INFO
|
default=JournalEntryKindChoices.KIND_INFO
|
||||||
)
|
)
|
||||||
comments = models.TextField()
|
comments = models.TextField(
|
||||||
|
verbose_name=_('comments'),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('-created',)
|
ordering = ('-created',)
|
||||||
@ -588,7 +626,7 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
|
|||||||
# Prevent the creation of journal entries on unsupported models
|
# Prevent the creation of journal entries on unsupported models
|
||||||
permitted_types = ContentType.objects.filter(FeatureQuery('journaling').get_query())
|
permitted_types = ContentType.objects.filter(FeatureQuery('journaling').get_query())
|
||||||
if self.assigned_object_type not in permitted_types:
|
if self.assigned_object_type not in permitted_types:
|
||||||
raise ValidationError(f"Journaling is not supported for this object type ({self.assigned_object_type}).")
|
raise ValidationError(_("Journaling is not supported for this object type ({type}).").format(type=self.assigned_object_type))
|
||||||
|
|
||||||
def get_kind_color(self):
|
def get_kind_color(self):
|
||||||
return JournalEntryKindChoices.colors.get(self.kind)
|
return JournalEntryKindChoices.colors.get(self.kind)
|
||||||
@ -599,6 +637,7 @@ class Bookmark(models.Model):
|
|||||||
An object bookmarked by a User.
|
An object bookmarked by a User.
|
||||||
"""
|
"""
|
||||||
created = models.DateTimeField(
|
created = models.DateTimeField(
|
||||||
|
verbose_name=_('created'),
|
||||||
auto_now_add=True
|
auto_now_add=True
|
||||||
)
|
)
|
||||||
object_type = models.ForeignKey(
|
object_type = models.ForeignKey(
|
||||||
@ -637,16 +676,18 @@ class ConfigRevision(models.Model):
|
|||||||
An atomic revision of NetBox's configuration.
|
An atomic revision of NetBox's configuration.
|
||||||
"""
|
"""
|
||||||
created = models.DateTimeField(
|
created = models.DateTimeField(
|
||||||
|
verbose_name=_('created'),
|
||||||
auto_now_add=True
|
auto_now_add=True
|
||||||
)
|
)
|
||||||
comment = models.CharField(
|
comment = models.CharField(
|
||||||
|
verbose_name=_('comment'),
|
||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
data = models.JSONField(
|
data = models.JSONField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name='Configuration data'
|
verbose_name=_('Configuration data')
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
Loading…
Reference in New Issue
Block a user