From 8b4b331ffd1a0f7ae4afd439418f11da994bb3e0 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 11 Jul 2023 15:30:01 +0700 Subject: [PATCH] 13132 add gettext_lazy to models --- netbox/extras/models/change_logging.py | 7 ++ netbox/extras/models/configs.py | 14 +++- netbox/extras/models/customfields.py | 108 ++++++++++++++++--------- netbox/extras/models/dashboard.py | 3 + netbox/extras/models/models.py | 77 +++++++++++++----- 5 files changed, 151 insertions(+), 58 deletions(-) diff --git a/netbox/extras/models/change_logging.py b/netbox/extras/models/change_logging.py index 444701acc..818629fd7 100644 --- a/netbox/extras/models/change_logging.py +++ b/netbox/extras/models/change_logging.py @@ -3,6 +3,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.urls import reverse +from django.utils.translation import gettext_lazy as _ from extras.choices import * 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. """ time = models.DateTimeField( + verbose_name=_('time'), auto_now_add=True, editable=False, db_index=True @@ -31,14 +33,17 @@ class ObjectChange(models.Model): null=True ) user_name = models.CharField( + verbose_name=_('user name'), max_length=150, editable=False ) request_id = models.UUIDField( + verbose_name=_('request id'), editable=False, db_index=True ) action = models.CharField( + verbose_name=_('action'), max_length=50, choices=ObjectChangeActionChoices ) @@ -72,11 +77,13 @@ class ObjectChange(models.Model): editable=False ) prechange_data = models.JSONField( + verbose_name=_('prechange data'), editable=False, blank=True, null=True ) postchange_data = models.JSONField( + verbose_name=_('postchange data'), editable=False, blank=True, null=True diff --git a/netbox/extras/models/configs.py b/netbox/extras/models/configs.py index ee9f7cfda..ce6ed3de2 100644 --- a/netbox/extras/models/configs.py +++ b/netbox/extras/models/configs.py @@ -2,7 +2,7 @@ from django.conf import settings from django.core.validators import ValidationError from django.db import models 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.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. """ name = models.CharField( + verbose_name=_('name'), max_length=100, unique=True ) weight = models.PositiveSmallIntegerField( + verbose_name=_('weight'), default=1000 ) description = models.CharField( + verbose_name=_('description'), max_length=200, blank=True ) is_active = models.BooleanField( + verbose_name=_('is active'), default=True, ) regions = models.ManyToManyField( @@ -138,7 +142,7 @@ class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel): # Verify that JSON data is provided as an object if type(self.data) is not dict: 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): @@ -194,7 +198,7 @@ class ConfigContextModel(models.Model): # Verify that JSON data is provided as an object if self.local_context_data and type(self.local_context_data) is not dict: 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): name = models.CharField( + verbose_name=_('name'), max_length=100 ) description = models.CharField( + verbose_name=_('description'), max_length=200, blank=True ) template_code = models.TextField( + verbose_name=_('template code'), help_text=_('Jinja2 template code.') ) environment_params = models.JSONField( + verbose_name=_('environment params'), blank=True, null=True, default=dict, diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index bdb600c88..4878ff282 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -12,7 +12,7 @@ from django.db import models from django.urls import reverse from django.utils.html import escape 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.utils import FeatureQuery @@ -64,6 +64,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): help_text=_('The object(s) to which this field applies.') ) type = models.CharField( + verbose_name=_('type'), max_length=50, choices=CustomFieldTypeChoices, 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)') ) name = models.CharField( + verbose_name=_('name'), max_length=50, unique=True, help_text=_('Internal field name'), validators=( RegexValidator( regex=r'^[a-z0-9_]+$', - message="Only alphanumeric characters and underscores are allowed.", + message=_("Only alphanumeric characters and underscores are allowed."), flags=re.IGNORECASE ), RegexValidator( 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, inverse_match=True ), ) ) label = models.CharField( + verbose_name=_('label'), max_length=50, blank=True, help_text=_('Name of the field as displayed to users (if not provided, ' 'the field\'s name will be used)') ) group_name = models.CharField( + verbose_name=_('group name'), max_length=50, blank=True, help_text=_("Custom fields within the same group will be displayed together") ) description = models.CharField( + verbose_name=_('description'), max_length=200, blank=True ) required = models.BooleanField( + verbose_name=_('required'), default=False, help_text=_('If true, this field is required when creating new objects ' 'or editing an existing object.') ) search_weight = models.PositiveSmallIntegerField( + verbose_name=_('search weight'), default=1000, help_text=_('Weighting for search. Lower values are considered more important. ' 'Fields with a search weight of zero will be ignored.') ) filter_logic = models.CharField( + verbose_name=_('filter logic'), max_length=50, choices=CustomFieldFilterLogicChoices, default=CustomFieldFilterLogicChoices.FILTER_LOOSE, @@ -127,6 +135,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): 'matches the entire field.') ) default = models.JSONField( + verbose_name=_('default'), blank=True, null=True, 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( default=100, - verbose_name='Display weight', + verbose_name=_('Display weight'), help_text=_('Fields with higher weights appear lower in a form.') ) validation_minimum = models.IntegerField( blank=True, null=True, - verbose_name='Minimum value', + verbose_name=_('Minimum value'), help_text=_('Minimum allowed value (for numeric fields)') ) validation_maximum = models.IntegerField( blank=True, null=True, - verbose_name='Maximum value', + verbose_name=_('Maximum value'), help_text=_('Maximum allowed value (for numeric fields)') ) validation_regex = models.CharField( blank=True, validators=[validate_regex], max_length=500, - verbose_name='Validation regex', + verbose_name=_('Validation regex'), help_text=_( 'Regular expression to enforce on text field values. Use ^ and $ to force matching of entire string. For ' 'example, ^[A-Z]{3}$ will limit values to exactly three uppercase letters.' ) ) +<<<<<<< HEAD choice_set = models.ForeignKey( to='CustomFieldChoiceSet', on_delete=models.PROTECT, 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, null=True ) @@ -170,12 +185,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): max_length=50, choices=CustomFieldVisibilityChoices, default=CustomFieldVisibilityChoices.VISIBILITY_READ_WRITE, - verbose_name='UI visibility', + verbose_name=_('UI visibility'), help_text=_('Specifies the visibility of custom field in the UI') ) is_cloneable = models.BooleanField( default=False, - verbose_name='Cloneable', + verbose_name=_('Cloneable'), help_text=_('Replicate this value when cloning objects') ) @@ -265,15 +280,15 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): self.validate(default_value) except ValidationError as err: 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 if self.type not in (CustomFieldTypeChoices.TYPE_INTEGER, CustomFieldTypeChoices.TYPE_DECIMAL): 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: - 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_types = ( @@ -283,10 +298,23 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): ) if self.validation_regex and self.type not in regex_types: 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 +======= + # 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 ( CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT @@ -297,24 +325,28 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): }) elif self.choice_set: raise ValidationError({ +<<<<<<< HEAD '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 if self.type == CustomFieldTypeChoices.TYPE_SELECT and self.default and self.default not in self.choices: 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 if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT): if not self.object_type: raise ValidationError({ - 'object_type': "Object fields must define an object type." + 'object_type': _("Object fields must define an object type.") }) elif self.object_type: 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): @@ -393,8 +425,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): elif self.type == CustomFieldTypeChoices.TYPE_BOOLEAN: choices = ( (None, '---------'), - (True, 'True'), - (False, 'False'), + (True, _('True')), + (False, _('False')), ) field = forms.NullBooleanField( required=required, initial=initial, widget=forms.Select(choices=choices) @@ -463,7 +495,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): field.validators = [ RegexValidator( regex=self.validation_regex, - message=mark_safe(f"Values must match this regex: {self.validation_regex}") + message=mark_safe(_("Values must match this regex: {regex}").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: field.disabled = True prepend = '
' if field.help_text else '' - field.help_text += f'{prepend} Field is set to read-only.' + field.help_text += f'{prepend} ' + _('Field is set to read-only.') return field @@ -558,33 +590,33 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): # Validate text field if self.type in (CustomFieldTypeChoices.TYPE_TEXT, CustomFieldTypeChoices.TYPE_LONGTEXT): 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): - raise ValidationError(f"Value must match regex '{self.validation_regex}'") + raise ValidationError(_("Value must match regex '{regex}'").format(regex=self.validation_regex)) # Validate integer elif self.type == CustomFieldTypeChoices.TYPE_INTEGER: 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: - 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: - 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 elif self.type == CustomFieldTypeChoices.TYPE_DECIMAL: try: decimal.Decimal(value) 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: - 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: - 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 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 elif self.type == CustomFieldTypeChoices.TYPE_DATE: @@ -592,7 +624,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): try: date.fromisoformat(value) 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 elif self.type == CustomFieldTypeChoices.TYPE_DATETIME: @@ -600,37 +632,38 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): try: datetime.fromisoformat(value) 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 elif self.type == CustomFieldTypeChoices.TYPE_SELECT: if value not in self.choices: 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 elif self.type == CustomFieldTypeChoices.TYPE_MULTISELECT: if not set(value).issubset(self.choices): 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 elif self.type == CustomFieldTypeChoices.TYPE_OBJECT: 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 elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: 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: 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: - raise ValidationError("Required field cannot be empty.") + raise ValidationError(_("Required field cannot be empty.")) class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): @@ -680,3 +713,4 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel self.extra_choices = sorted(self.choices) return super().save(*args, **kwargs) + diff --git a/netbox/extras/models/dashboard.py b/netbox/extras/models/dashboard.py index a0d300ff6..33bb735c4 100644 --- a/netbox/extras/models/dashboard.py +++ b/netbox/extras/models/dashboard.py @@ -1,5 +1,6 @@ from django.contrib.auth import get_user_model from django.db import models +from django.utils.translation import gettext_lazy as _ from extras.dashboard.utils import get_widget_class @@ -15,9 +16,11 @@ class Dashboard(models.Model): related_name='dashboard' ) layout = models.JSONField( + verbose_name=_('layout'), default=list ) config = models.JSONField( + verbose_name=_('config'), default=dict ) diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 193d3af6a..33837a027 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -12,7 +12,7 @@ from django.http import HttpResponse from django.urls import reverse from django.utils import timezone 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 extras.choices import * @@ -53,64 +53,74 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel): help_text=_("The object(s) to which this Webhook applies.") ) name = models.CharField( + verbose_name=_('name'), max_length=150, unique=True ) type_create = models.BooleanField( + verbose_name=_('type create'), default=False, help_text=_("Triggers when a matching object is created.") ) type_update = models.BooleanField( + verbose_name=_('type update'), default=False, help_text=_("Triggers when a matching object is updated.") ) type_delete = models.BooleanField( + verbose_name=_('type delete'), default=False, help_text=_("Triggers when a matching object is deleted.") ) type_job_start = models.BooleanField( + verbose_name=_('type job start'), default=False, help_text=_("Triggers when a job for a matching object is started.") ) type_job_end = models.BooleanField( + verbose_name=_('type job end'), default=False, help_text=_("Triggers when a job for a matching object terminates.") ) payload_url = models.CharField( 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. ' 'Jinja2 template processing is supported with the same context as the request body.') ) enabled = models.BooleanField( + verbose_name=_('enabled'), default=True ) http_method = models.CharField( max_length=30, choices=WebhookHttpMethodChoices, default=WebhookHttpMethodChoices.METHOD_POST, - verbose_name='HTTP method' + verbose_name=_('HTTP method') ) http_content_type = models.CharField( max_length=100, 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 ' 'here.') ) additional_headers = models.TextField( + verbose_name=_('additional headers'), blank=True, 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 Name: Value. Jinja2 template processing is " "supported with the same context as the request body (below).") ) body_template = models.TextField( + verbose_name=_('body template'), blank=True, help_text=_('Jinja2 template for a custom request body. If blank, a JSON object representing the change will be ' 'included. Available context data includes: event, model, ' 'timestamp, username, request_id, and data.') ) secret = models.CharField( + verbose_name=_('secret'), max_length=255, blank=True, help_text=_("When provided, the request will include a 'X-Hook-Signature' " @@ -119,20 +129,21 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel): "the request.") ) conditions = models.JSONField( + verbose_name=_('conditions'), blank=True, null=True, help_text=_("A set of conditions which determine whether the webhook will be generated.") ) ssl_verification = models.BooleanField( default=True, - verbose_name='SSL verification', + verbose_name=_('SSL verification'), help_text=_("Enable SSL certificate verification. Disable with caution!") ) ca_file_path = models.CharField( max_length=4096, null=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. ' '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 ]): 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: @@ -176,7 +187,7 @@ class Webhook(ExportTemplatesMixin, ChangeLoggedModel): # CA file path requires SSL verification enabled if not self.ssl_verification and self.ca_file_path: 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): @@ -219,34 +230,41 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): help_text=_('The object type(s) to which this link applies.') ) name = models.CharField( + verbose_name=_('name'), max_length=100, unique=True ) enabled = models.BooleanField( + verbose_name=_('enabled'), default=True ) link_text = models.TextField( + verbose_name=_('link text'), help_text=_("Jinja2 template code for link text") ) link_url = models.TextField( - verbose_name='Link URL', + verbose_name=_('Link URL'), help_text=_("Jinja2 template code for link URL") ) weight = models.PositiveSmallIntegerField( + verbose_name=_('weight'), default=100 ) group_name = models.CharField( + verbose_name=_('group name'), max_length=50, blank=True, help_text=_("Links with the same group will appear as a dropdown menu") ) button_class = models.CharField( + verbose_name=_('button class'), max_length=30, choices=CustomLinkButtonClassChoices, default=CustomLinkButtonClassChoices.DEFAULT, help_text=_("The class of the first link in a group will be used for the dropdown button") ) new_window = models.BooleanField( + verbose_name=_('new window'), default=False, 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.') ) name = models.CharField( + verbose_name=_('name'), max_length=100 ) description = models.CharField( + verbose_name=_('description'), max_length=200, blank=True ) @@ -319,15 +339,17 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change mime_type = models.CharField( max_length=50, blank=True, - verbose_name='MIME type', + verbose_name=_('MIME type'), help_text=_('Defaults to text/plain; charset=utf-8') ) file_extension = models.CharField( + verbose_name=_('file extension'), max_length=15, blank=True, help_text=_('Extension to append to the rendered filename') ) as_attachment = models.BooleanField( + verbose_name=_('as attachment'), default=True, help_text=_("Download file as attachment") ) @@ -354,7 +376,7 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change if self.name.lower() == 'table': 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): @@ -407,14 +429,17 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): help_text=_('The object type(s) to which this filter applies.') ) name = models.CharField( + verbose_name=_('name'), max_length=100, unique=True ) slug = models.SlugField( + verbose_name=_('slug'), max_length=100, unique=True ) description = models.CharField( + verbose_name=_('description'), max_length=200, blank=True ) @@ -425,15 +450,20 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): null=True ) weight = models.PositiveSmallIntegerField( + verbose_name=_('weight'), default=100 ) enabled = models.BooleanField( + verbose_name=_('enabled'), default=True ) shared = models.BooleanField( + verbose_name=_('shared'), default=True ) - parameters = models.JSONField() + parameters = models.JSONField( + verbose_name=_('parameters') + ) clone_fields = ( 'content_types', 'weight', 'enabled', 'parameters', @@ -458,7 +488,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): # Verify that `parameters` is a JSON object if type(self.parameters) is not dict: 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 @@ -485,9 +515,14 @@ class ImageAttachment(ChangeLoggedModel): height_field='image_height', width_field='image_width' ) - image_height = models.PositiveSmallIntegerField() - image_width = models.PositiveSmallIntegerField() + image_height = models.PositiveSmallIntegerField( + verbose_name=_('image height'), + ) + image_width = models.PositiveSmallIntegerField( + verbose_name=_('image width'), + ) name = models.CharField( + verbose_name=_('name'), max_length=50, blank=True ) @@ -565,11 +600,14 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat null=True ) kind = models.CharField( + verbose_name=_('kind'), max_length=30, choices=JournalEntryKindChoices, default=JournalEntryKindChoices.KIND_INFO ) - comments = models.TextField() + comments = models.TextField( + verbose_name=_('comments'), + ) class Meta: ordering = ('-created',) @@ -588,7 +626,7 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat # Prevent the creation of journal entries on unsupported models permitted_types = ContentType.objects.filter(FeatureQuery('journaling').get_query()) 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): return JournalEntryKindChoices.colors.get(self.kind) @@ -599,6 +637,7 @@ class Bookmark(models.Model): An object bookmarked by a User. """ created = models.DateTimeField( + verbose_name=_('created'), auto_now_add=True ) object_type = models.ForeignKey( @@ -637,16 +676,18 @@ class ConfigRevision(models.Model): An atomic revision of NetBox's configuration. """ created = models.DateTimeField( + verbose_name=_('created'), auto_now_add=True ) comment = models.CharField( + verbose_name=_('comment'), max_length=200, blank=True ) data = models.JSONField( blank=True, null=True, - verbose_name='Configuration data' + verbose_name=_('Configuration data') ) objects = RestrictedQuerySet.as_manager()