prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Empty search entries are being created for device asset tags
  ### Deployment Type
  
  NetBox Cloud
  
  ### NetBox Version
  
  v3.7.4
  
  ### Python Version
  
  3.11
  
  ### Steps to Reproduce
  
  1. Create a new device and note its database ID
  2. In the NetBox shell, inspect all search entries associated with it:
  
  ```python
  ct = ContentType.objects.get_for_model(Device)
  device_id = 107
  entries = CachedValue.objects.filter(object_type=ct, object_id=device_id)
  for entry in entries:
      print(f'{entry.field}: {entry.value}')
  ```
  
  ### Expected Behavior
  
  Only fields which have a meaningful value set should have search entries created.
  
  ### Observed Behavior
  
  After creating a device with a description, I see three entries for it:
  
  ```
  asset_tag: None
  name: device1
  description: asdasdasd
  ```
  
  The value of `asset_tag` is null.
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `netbox/netbox/search/__init__.py`
  Content:
  ```
  1 from collections import namedtuple
  2 
  3 from django.db import models
  4 
  5 from ipam.fields import IPAddressField, IPNetworkField
  6 from netbox.registry import registry
  7 
  8 ObjectFieldValue = namedtuple('ObjectFieldValue', ('name', 'type', 'weight', 'value'))
  9 
  10 
  11 class FieldTypes:
  12     FLOAT = 'float'
  13     INTEGER = 'int'
  14     STRING = 'str'
  15     INET = 'inet'
  16     CIDR = 'cidr'
  17 
  18 
  19 class LookupTypes:
  20     PARTIAL = 'icontains'
  21     EXACT = 'iexact'
  22     STARTSWITH = 'istartswith'
  23     ENDSWITH = 'iendswith'
  24     REGEX = 'iregex'
  25 
  26 
  27 class SearchIndex:
  28     """
  29     Base class for building search indexes.
  30 
  31     Attributes:
  32         model: The model class for which this index is used.
  33         category: The label of the group under which this indexer is categorized (for form field display). If none,
  34             the name of the model's app will be used.
  35         fields: An iterable of two-tuples defining the model fields to be indexed and the weight associated with each.
  36         display_attrs: An iterable of additional object attributes to include when displaying search results.
  37     """
  38     model = None
  39     category = None
  40     fields = ()
  41     display_attrs = ()
  42 
  43     @staticmethod
  44     def get_field_type(instance, field_name):
  45         """
  46         Return the data type of the specified model field.
  47         """
  48         field_cls = instance._meta.get_field(field_name).__class__
  49         if issubclass(field_cls, (models.FloatField, models.DecimalField)):
  50             return FieldTypes.FLOAT
  51         if issubclass(field_cls, IPAddressField):
  52             return FieldTypes.INET
  53         if issubclass(field_cls, IPNetworkField):
  54             return FieldTypes.CIDR
  55         if issubclass(field_cls, models.IntegerField):
  56             return FieldTypes.INTEGER
  57         return FieldTypes.STRING
  58 
  59     @staticmethod
  60     def get_field_value(instance, field_name):
  61         """
  62         Return the value of the specified model field as a string.
  63         """
  64         return str(getattr(instance, field_name))
  65 
  66     @classmethod
  67     def get_category(cls):
  68         return cls.category or cls.model._meta.app_config.verbose_name
  69 
  70     @classmethod
  71     def to_cache(cls, instance, custom_fields=None):
  72         """
  73         Return a list of ObjectFieldValue representing the instance fields to be cached.
  74 
  75         Args:
  76             instance: The instance being cached.
  77             custom_fields: An iterable of CustomFields to include when caching the instance. If None, all custom fields
  78                 defined for the model will be included. (This can also be provided during bulk caching to avoid looking
  79                 up the available custom fields for each instance.)
  80         """
  81         values = []
  82 
  83         # Capture built-in fields
  84         for name, weight in cls.fields:
  85             type_ = cls.get_field_type(instance, name)
  86             value = cls.get_field_value(instance, name)
  87             if type_ and value:
  88                 values.append(
  89                     ObjectFieldValue(name, type_, weight, value)
  90                 )
  91 
  92         # Capture custom fields
  93         if getattr(instance, 'custom_field_data', None):
  94             if custom_fields is None:
  95                 custom_fields = instance.custom_fields
  96             for cf in custom_fields:
  97                 type_ = cf.search_type
  98                 value = instance.custom_field_data.get(cf.name)
  99                 weight = cf.search_weight
  100                 if type_ and value and weight:
  101                     values.append(
  102                         ObjectFieldValue(f'cf_{cf.name}', type_, weight, value)
  103                     )
  104 
  105         return values
  106 
  107 
  108 def get_indexer(model):
  109     """
  110     Get the SearchIndex class for the given model.
  111     """
  112     label = f'{model._meta.app_label}.{model._meta.model_name}'
  113 
  114     return registry['search'][label]
  115 
  116 
  117 def register_search(cls):
  118     """
  119     Decorator for registering a SearchIndex class.
  120     """
  121     model = cls.model
  122     label = f'{model._meta.app_label}.{model._meta.model_name}'
  123     registry['search'][label] = cls
  124 
  125     return cls
  126
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/netbox/netbox/search/__init__.py b/netbox/netbox/search/__init__.py
  --- a/netbox/netbox/search/__init__.py
  +++ b/netbox/netbox/search/__init__.py
  @@ -59,9 +59,10 @@
       @staticmethod
       def get_field_value(instance, field_name):
           """
  -        Return the value of the specified model field as a string.
  +        Return the value of the specified model field as a string (or None).
           """
  -        return str(getattr(instance, field_name))
  +        if value := getattr(instance, field_name):
  +            return str(value)
   
       @classmethod
       def get_category(cls):
  
verification_info: {"golden_diff": "diff --git a/netbox/netbox/search/__init__.py b/netbox/netbox/search/__init__.py\n--- a/netbox/netbox/search/__init__.py\n+++ b/netbox/netbox/search/__init__.py\n@@ -59,9 +59,10 @@\n     @staticmethod\n     def get_field_value(instance, field_name):\n         \"\"\"\n-        Return the value of the specified model field as a string.\n+        Return the value of the specified model field as a string (or None).\n         \"\"\"\n-        return str(getattr(instance, field_name))\n+        if value := getattr(instance, field_name):\n+            return str(value)\n \n     @classmethod\n     def get_category(cls):\n", "issue": "Empty search entries are being created for device asset tags\n### Deployment Type\n\nNetBox Cloud\n\n### NetBox Version\n\nv3.7.4\n\n### Python Version\n\n3.11\n\n### Steps to Reproduce\n\n1. Create a new device and note its database ID\r\n2. In the NetBox shell, inspect all search entries associated with it:\r\n\r\n```python\r\nct = ContentType.objects.get_for_model(Device)\r\ndevice_id = 107\r\nentries = CachedValue.objects.filter(object_type=ct, object_id=device_id)\r\nfor entry in entries:\r\n    print(f'{entry.field}: {entry.value}')\r\n```\n\n### Expected Behavior\n\nOnly fields which have a meaningful value set should have search entries created.\n\n### Observed Behavior\n\nAfter creating a device with a description, I see three entries for it:\r\n\r\n```\r\nasset_tag: None\r\nname: device1\r\ndescription: asdasdasd\r\n```\r\n\r\nThe value of `asset_tag` is null.\n", "before_files": [{"content": "from collections import namedtuple\n\nfrom django.db import models\n\nfrom ipam.fields import IPAddressField, IPNetworkField\nfrom netbox.registry import registry\n\nObjectFieldValue = namedtuple('ObjectFieldValue', ('name', 'type', 'weight', 'value'))\n\n\nclass FieldTypes:\n    FLOAT = 'float'\n    INTEGER = 'int'\n    STRING = 'str'\n    INET = 'inet'\n    CIDR = 'cidr'\n\n\nclass LookupTypes:\n    PARTIAL = 'icontains'\n    EXACT = 'iexact'\n    STARTSWITH = 'istartswith'\n    ENDSWITH = 'iendswith'\n    REGEX = 'iregex'\n\n\nclass SearchIndex:\n    \"\"\"\n    Base class for building search indexes.\n\n    Attributes:\n        model: The model class for which this index is used.\n        category: The label of the group under which this indexer is categorized (for form field display). If none,\n            the name of the model's app will be used.\n        fields: An iterable of two-tuples defining the model fields to be indexed and the weight associated with each.\n        display_attrs: An iterable of additional object attributes to include when displaying search results.\n    \"\"\"\n    model = None\n    category = None\n    fields = ()\n    display_attrs = ()\n\n    @staticmethod\n    def get_field_type(instance, field_name):\n        \"\"\"\n        Return the data type of the specified model field.\n        \"\"\"\n        field_cls = instance._meta.get_field(field_name).__class__\n        if issubclass(field_cls, (models.FloatField, models.DecimalField)):\n            return FieldTypes.FLOAT\n        if issubclass(field_cls, IPAddressField):\n            return FieldTypes.INET\n        if issubclass(field_cls, IPNetworkField):\n            return FieldTypes.CIDR\n        if issubclass(field_cls, models.IntegerField):\n            return FieldTypes.INTEGER\n        return FieldTypes.STRING\n\n    @staticmethod\n    def get_field_value(instance, field_name):\n        \"\"\"\n        Return the value of the specified model field as a string.\n        \"\"\"\n        return str(getattr(instance, field_name))\n\n    @classmethod\n    def get_category(cls):\n        return cls.category or cls.model._meta.app_config.verbose_name\n\n    @classmethod\n    def to_cache(cls, instance, custom_fields=None):\n        \"\"\"\n        Return a list of ObjectFieldValue representing the instance fields to be cached.\n\n        Args:\n            instance: The instance being cached.\n            custom_fields: An iterable of CustomFields to include when caching the instance. If None, all custom fields\n                defined for the model will be included. (This can also be provided during bulk caching to avoid looking\n                up the available custom fields for each instance.)\n        \"\"\"\n        values = []\n\n        # Capture built-in fields\n        for name, weight in cls.fields:\n            type_ = cls.get_field_type(instance, name)\n            value = cls.get_field_value(instance, name)\n            if type_ and value:\n                values.append(\n                    ObjectFieldValue(name, type_, weight, value)\n                )\n\n        # Capture custom fields\n        if getattr(instance, 'custom_field_data', None):\n            if custom_fields is None:\n                custom_fields = instance.custom_fields\n            for cf in custom_fields:\n                type_ = cf.search_type\n                value = instance.custom_field_data.get(cf.name)\n                weight = cf.search_weight\n                if type_ and value and weight:\n                    values.append(\n                        ObjectFieldValue(f'cf_{cf.name}', type_, weight, value)\n                    )\n\n        return values\n\n\ndef get_indexer(model):\n    \"\"\"\n    Get the SearchIndex class for the given model.\n    \"\"\"\n    label = f'{model._meta.app_label}.{model._meta.model_name}'\n\n    return registry['search'][label]\n\n\ndef register_search(cls):\n    \"\"\"\n    Decorator for registering a SearchIndex class.\n    \"\"\"\n    model = cls.model\n    label = f'{model._meta.app_label}.{model._meta.model_name}'\n    registry['search'][label] = cls\n\n    return cls\n", "path": "netbox/netbox/search/__init__.py"}], "after_files": [{"content": "from collections import namedtuple\n\nfrom django.db import models\n\nfrom ipam.fields import IPAddressField, IPNetworkField\nfrom netbox.registry import registry\n\nObjectFieldValue = namedtuple('ObjectFieldValue', ('name', 'type', 'weight', 'value'))\n\n\nclass FieldTypes:\n    FLOAT = 'float'\n    INTEGER = 'int'\n    STRING = 'str'\n    INET = 'inet'\n    CIDR = 'cidr'\n\n\nclass LookupTypes:\n    PARTIAL = 'icontains'\n    EXACT = 'iexact'\n    STARTSWITH = 'istartswith'\n    ENDSWITH = 'iendswith'\n    REGEX = 'iregex'\n\n\nclass SearchIndex:\n    \"\"\"\n    Base class for building search indexes.\n\n    Attributes:\n        model: The model class for which this index is used.\n        category: The label of the group under which this indexer is categorized (for form field display). If none,\n            the name of the model's app will be used.\n        fields: An iterable of two-tuples defining the model fields to be indexed and the weight associated with each.\n        display_attrs: An iterable of additional object attributes to include when displaying search results.\n    \"\"\"\n    model = None\n    category = None\n    fields = ()\n    display_attrs = ()\n\n    @staticmethod\n    def get_field_type(instance, field_name):\n        \"\"\"\n        Return the data type of the specified model field.\n        \"\"\"\n        field_cls = instance._meta.get_field(field_name).__class__\n        if issubclass(field_cls, (models.FloatField, models.DecimalField)):\n            return FieldTypes.FLOAT\n        if issubclass(field_cls, IPAddressField):\n            return FieldTypes.INET\n        if issubclass(field_cls, IPNetworkField):\n            return FieldTypes.CIDR\n        if issubclass(field_cls, models.IntegerField):\n            return FieldTypes.INTEGER\n        return FieldTypes.STRING\n\n    @staticmethod\n    def get_field_value(instance, field_name):\n        \"\"\"\n        Return the value of the specified model field as a string (or None).\n        \"\"\"\n        if value := getattr(instance, field_name):\n            return str(value)\n\n    @classmethod\n    def get_category(cls):\n        return cls.category or cls.model._meta.app_config.verbose_name\n\n    @classmethod\n    def to_cache(cls, instance, custom_fields=None):\n        \"\"\"\n        Return a list of ObjectFieldValue representing the instance fields to be cached.\n\n        Args:\n            instance: The instance being cached.\n            custom_fields: An iterable of CustomFields to include when caching the instance. If None, all custom fields\n                defined for the model will be included. (This can also be provided during bulk caching to avoid looking\n                up the available custom fields for each instance.)\n        \"\"\"\n        values = []\n\n        # Capture built-in fields\n        for name, weight in cls.fields:\n            type_ = cls.get_field_type(instance, name)\n            value = cls.get_field_value(instance, name)\n            if type_ and value:\n                values.append(\n                    ObjectFieldValue(name, type_, weight, value)\n                )\n\n        # Capture custom fields\n        if getattr(instance, 'custom_field_data', None):\n            if custom_fields is None:\n                custom_fields = instance.custom_fields\n            for cf in custom_fields:\n                type_ = cf.search_type\n                value = instance.custom_field_data.get(cf.name)\n                weight = cf.search_weight\n                if type_ and value and weight:\n                    values.append(\n                        ObjectFieldValue(f'cf_{cf.name}', type_, weight, value)\n                    )\n\n        return values\n\n\ndef get_indexer(model):\n    \"\"\"\n    Get the SearchIndex class for the given model.\n    \"\"\"\n    label = f'{model._meta.app_label}.{model._meta.model_name}'\n\n    return registry['search'][label]\n\n\ndef register_search(cls):\n    \"\"\"\n    Decorator for registering a SearchIndex class.\n    \"\"\"\n    model = cls.model\n    label = f'{model._meta.app_label}.{model._meta.model_name}'\n    registry['search'][label] = cls\n\n    return cls\n", "path": "netbox/netbox/search/__init__.py"}]}