serializers.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. from django.contrib.auth import get_user_model
  2. from django.core.exceptions import ObjectDoesNotExist
  3. from drf_spectacular.types import OpenApiTypes
  4. from drf_spectacular.utils import extend_schema_field
  5. from rest_framework import serializers
  6. from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer
  7. from core.api.serializers import JobSerializer
  8. from core.models import ContentType
  9. from dcim.api.nested_serializers import (
  10. NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer,
  11. NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
  12. )
  13. from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
  14. from extras.choices import *
  15. from extras.models import *
  16. from netbox.api.exceptions import SerializerNotFound
  17. from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
  18. from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
  19. from netbox.api.serializers.features import TaggableModelSerializer
  20. from netbox.constants import NESTED_SERIALIZER_PREFIX
  21. from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
  22. from tenancy.models import Tenant, TenantGroup
  23. from users.api.nested_serializers import NestedUserSerializer
  24. from utilities.api import get_serializer_for_model
  25. from virtualization.api.nested_serializers import (
  26. NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer,
  27. )
  28. from virtualization.models import Cluster, ClusterGroup, ClusterType
  29. from .nested_serializers import *
  30. __all__ = (
  31. 'BookmarkSerializer',
  32. 'ConfigContextSerializer',
  33. 'ConfigTemplateSerializer',
  34. 'ContentTypeSerializer',
  35. 'CustomFieldChoiceSetSerializer',
  36. 'CustomFieldSerializer',
  37. 'CustomLinkSerializer',
  38. 'DashboardSerializer',
  39. 'EventRuleSerializer',
  40. 'ExportTemplateSerializer',
  41. 'ImageAttachmentSerializer',
  42. 'JournalEntrySerializer',
  43. 'ObjectChangeSerializer',
  44. 'ReportDetailSerializer',
  45. 'ReportSerializer',
  46. 'ReportInputSerializer',
  47. 'SavedFilterSerializer',
  48. 'ScriptDetailSerializer',
  49. 'ScriptInputSerializer',
  50. 'ScriptLogMessageSerializer',
  51. 'ScriptOutputSerializer',
  52. 'ScriptSerializer',
  53. 'TagSerializer',
  54. 'WebhookSerializer',
  55. )
  56. #
  57. # Event Rules
  58. #
  59. class EventRuleSerializer(NetBoxModelSerializer):
  60. url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
  61. content_types = ContentTypeField(
  62. queryset=ContentType.objects.with_feature('event_rules'),
  63. many=True
  64. )
  65. action_type = ChoiceField(choices=EventRuleActionChoices)
  66. action_object_type = ContentTypeField(
  67. queryset=ContentType.objects.with_feature('event_rules'),
  68. )
  69. action_object = serializers.SerializerMethodField(read_only=True)
  70. class Meta:
  71. model = EventRule
  72. fields = [
  73. 'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete',
  74. 'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type',
  75. 'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated',
  76. ]
  77. @extend_schema_field(OpenApiTypes.OBJECT)
  78. def get_action_object(self, instance):
  79. context = {'request': self.context['request']}
  80. # We need to manually instantiate the serializer for scripts
  81. if instance.action_type == EventRuleActionChoices.SCRIPT:
  82. script_name = instance.action_parameters['script_name']
  83. script = instance.action_object.scripts[script_name]()
  84. return NestedScriptSerializer(script, context=context).data
  85. else:
  86. serializer = get_serializer_for_model(
  87. model=instance.action_object_type.model_class(),
  88. prefix=NESTED_SERIALIZER_PREFIX
  89. )
  90. return serializer(instance.action_object, context=context).data
  91. #
  92. # Webhooks
  93. #
  94. class WebhookSerializer(NetBoxModelSerializer):
  95. url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
  96. class Meta:
  97. model = Webhook
  98. fields = [
  99. 'id', 'url', 'display', 'name', 'description', 'payload_url', 'http_method', 'http_content_type',
  100. 'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields',
  101. 'tags', 'created', 'last_updated',
  102. ]
  103. #
  104. # Custom fields
  105. #
  106. class CustomFieldSerializer(ValidatedModelSerializer):
  107. url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
  108. content_types = ContentTypeField(
  109. queryset=ContentType.objects.with_feature('custom_fields'),
  110. many=True
  111. )
  112. type = ChoiceField(choices=CustomFieldTypeChoices)
  113. object_type = ContentTypeField(
  114. queryset=ContentType.objects.all(),
  115. required=False,
  116. allow_null=True
  117. )
  118. filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
  119. data_type = serializers.SerializerMethodField()
  120. choice_set = NestedCustomFieldChoiceSetSerializer(
  121. required=False,
  122. allow_null=True
  123. )
  124. ui_visible = ChoiceField(choices=CustomFieldUIVisibleChoices, required=False)
  125. ui_editable = ChoiceField(choices=CustomFieldUIEditableChoices, required=False)
  126. class Meta:
  127. model = CustomField
  128. fields = [
  129. 'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
  130. 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
  131. 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set',
  132. 'created', 'last_updated',
  133. ]
  134. def validate_type(self, value):
  135. if self.instance and self.instance.type != value:
  136. raise serializers.ValidationError('Changing the type of custom fields is not supported.')
  137. return value
  138. @extend_schema_field(OpenApiTypes.STR)
  139. def get_data_type(self, obj):
  140. types = CustomFieldTypeChoices
  141. if obj.type == types.TYPE_INTEGER:
  142. return 'integer'
  143. if obj.type == types.TYPE_DECIMAL:
  144. return 'decimal'
  145. if obj.type == types.TYPE_BOOLEAN:
  146. return 'boolean'
  147. if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT):
  148. return 'object'
  149. if obj.type in (types.TYPE_MULTISELECT, types.TYPE_MULTIOBJECT):
  150. return 'array'
  151. return 'string'
  152. class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
  153. url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
  154. base_choices = ChoiceField(
  155. choices=CustomFieldChoiceSetBaseChoices,
  156. required=False
  157. )
  158. class Meta:
  159. model = CustomFieldChoiceSet
  160. fields = [
  161. 'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
  162. 'choices_count', 'created', 'last_updated',
  163. ]
  164. #
  165. # Custom links
  166. #
  167. class CustomLinkSerializer(ValidatedModelSerializer):
  168. url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
  169. content_types = ContentTypeField(
  170. queryset=ContentType.objects.with_feature('custom_links'),
  171. many=True
  172. )
  173. class Meta:
  174. model = CustomLink
  175. fields = [
  176. 'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
  177. 'button_class', 'new_window', 'created', 'last_updated',
  178. ]
  179. #
  180. # Export templates
  181. #
  182. class ExportTemplateSerializer(ValidatedModelSerializer):
  183. url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
  184. content_types = ContentTypeField(
  185. queryset=ContentType.objects.with_feature('export_templates'),
  186. many=True
  187. )
  188. data_source = NestedDataSourceSerializer(
  189. required=False
  190. )
  191. data_file = NestedDataFileSerializer(
  192. read_only=True
  193. )
  194. class Meta:
  195. model = ExportTemplate
  196. fields = [
  197. 'id', 'url', 'display', 'content_types', 'name', 'description', 'template_code', 'mime_type',
  198. 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created',
  199. 'last_updated',
  200. ]
  201. #
  202. # Saved filters
  203. #
  204. class SavedFilterSerializer(ValidatedModelSerializer):
  205. url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
  206. content_types = ContentTypeField(
  207. queryset=ContentType.objects.all(),
  208. many=True
  209. )
  210. class Meta:
  211. model = SavedFilter
  212. fields = [
  213. 'id', 'url', 'display', 'content_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled',
  214. 'shared', 'parameters', 'created', 'last_updated',
  215. ]
  216. #
  217. # Bookmarks
  218. #
  219. class BookmarkSerializer(ValidatedModelSerializer):
  220. url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
  221. object_type = ContentTypeField(
  222. queryset=ContentType.objects.with_feature('bookmarks'),
  223. )
  224. object = serializers.SerializerMethodField(read_only=True)
  225. user = NestedUserSerializer()
  226. class Meta:
  227. model = Bookmark
  228. fields = [
  229. 'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created',
  230. ]
  231. @extend_schema_field(serializers.JSONField(allow_null=True))
  232. def get_object(self, instance):
  233. serializer = get_serializer_for_model(instance.object, prefix=NESTED_SERIALIZER_PREFIX)
  234. return serializer(instance.object, context={'request': self.context['request']}).data
  235. #
  236. # Tags
  237. #
  238. class TagSerializer(ValidatedModelSerializer):
  239. url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
  240. object_types = ContentTypeField(
  241. queryset=ContentType.objects.with_feature('tags'),
  242. many=True,
  243. required=False
  244. )
  245. tagged_items = serializers.IntegerField(read_only=True)
  246. class Meta:
  247. model = Tag
  248. fields = [
  249. 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created',
  250. 'last_updated',
  251. ]
  252. #
  253. # Image attachments
  254. #
  255. class ImageAttachmentSerializer(ValidatedModelSerializer):
  256. url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
  257. content_type = ContentTypeField(
  258. queryset=ContentType.objects.all()
  259. )
  260. parent = serializers.SerializerMethodField(read_only=True)
  261. class Meta:
  262. model = ImageAttachment
  263. fields = [
  264. 'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height',
  265. 'image_width', 'created', 'last_updated',
  266. ]
  267. def validate(self, data):
  268. # Validate that the parent object exists
  269. try:
  270. data['content_type'].get_object_for_this_type(id=data['object_id'])
  271. except ObjectDoesNotExist:
  272. raise serializers.ValidationError(
  273. "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
  274. )
  275. # Enforce model validation
  276. super().validate(data)
  277. return data
  278. @extend_schema_field(serializers.JSONField(allow_null=True))
  279. def get_parent(self, obj):
  280. serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
  281. return serializer(obj.parent, context={'request': self.context['request']}).data
  282. #
  283. # Journal entries
  284. #
  285. class JournalEntrySerializer(NetBoxModelSerializer):
  286. url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
  287. assigned_object_type = ContentTypeField(
  288. queryset=ContentType.objects.all()
  289. )
  290. assigned_object = serializers.SerializerMethodField(read_only=True)
  291. created_by = serializers.PrimaryKeyRelatedField(
  292. allow_null=True,
  293. queryset=get_user_model().objects.all(),
  294. required=False,
  295. default=serializers.CurrentUserDefault()
  296. )
  297. kind = ChoiceField(
  298. choices=JournalEntryKindChoices,
  299. required=False
  300. )
  301. class Meta:
  302. model = JournalEntry
  303. fields = [
  304. 'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
  305. 'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
  306. ]
  307. def validate(self, data):
  308. # Validate that the parent object exists
  309. if 'assigned_object_type' in data and 'assigned_object_id' in data:
  310. try:
  311. data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id'])
  312. except ObjectDoesNotExist:
  313. raise serializers.ValidationError(
  314. f"Invalid assigned_object: {data['assigned_object_type']} ID {data['assigned_object_id']}"
  315. )
  316. # Enforce model validation
  317. super().validate(data)
  318. return data
  319. @extend_schema_field(serializers.JSONField(allow_null=True))
  320. def get_assigned_object(self, instance):
  321. serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
  322. context = {'request': self.context['request']}
  323. return serializer(instance.assigned_object, context=context).data
  324. #
  325. # Config contexts
  326. #
  327. class ConfigContextSerializer(ValidatedModelSerializer):
  328. url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail')
  329. regions = SerializedPKRelatedField(
  330. queryset=Region.objects.all(),
  331. serializer=NestedRegionSerializer,
  332. required=False,
  333. many=True
  334. )
  335. site_groups = SerializedPKRelatedField(
  336. queryset=SiteGroup.objects.all(),
  337. serializer=NestedSiteGroupSerializer,
  338. required=False,
  339. many=True
  340. )
  341. sites = SerializedPKRelatedField(
  342. queryset=Site.objects.all(),
  343. serializer=NestedSiteSerializer,
  344. required=False,
  345. many=True
  346. )
  347. locations = SerializedPKRelatedField(
  348. queryset=Location.objects.all(),
  349. serializer=NestedLocationSerializer,
  350. required=False,
  351. many=True
  352. )
  353. device_types = SerializedPKRelatedField(
  354. queryset=DeviceType.objects.all(),
  355. serializer=NestedDeviceTypeSerializer,
  356. required=False,
  357. many=True
  358. )
  359. roles = SerializedPKRelatedField(
  360. queryset=DeviceRole.objects.all(),
  361. serializer=NestedDeviceRoleSerializer,
  362. required=False,
  363. many=True
  364. )
  365. platforms = SerializedPKRelatedField(
  366. queryset=Platform.objects.all(),
  367. serializer=NestedPlatformSerializer,
  368. required=False,
  369. many=True
  370. )
  371. cluster_types = SerializedPKRelatedField(
  372. queryset=ClusterType.objects.all(),
  373. serializer=NestedClusterTypeSerializer,
  374. required=False,
  375. many=True
  376. )
  377. cluster_groups = SerializedPKRelatedField(
  378. queryset=ClusterGroup.objects.all(),
  379. serializer=NestedClusterGroupSerializer,
  380. required=False,
  381. many=True
  382. )
  383. clusters = SerializedPKRelatedField(
  384. queryset=Cluster.objects.all(),
  385. serializer=NestedClusterSerializer,
  386. required=False,
  387. many=True
  388. )
  389. tenant_groups = SerializedPKRelatedField(
  390. queryset=TenantGroup.objects.all(),
  391. serializer=NestedTenantGroupSerializer,
  392. required=False,
  393. many=True
  394. )
  395. tenants = SerializedPKRelatedField(
  396. queryset=Tenant.objects.all(),
  397. serializer=NestedTenantSerializer,
  398. required=False,
  399. many=True
  400. )
  401. tags = serializers.SlugRelatedField(
  402. queryset=Tag.objects.all(),
  403. slug_field='slug',
  404. required=False,
  405. many=True
  406. )
  407. data_source = NestedDataSourceSerializer(
  408. required=False
  409. )
  410. data_file = NestedDataFileSerializer(
  411. read_only=True
  412. )
  413. class Meta:
  414. model = ConfigContext
  415. fields = [
  416. 'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
  417. 'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters',
  418. 'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', 'data_synced', 'data',
  419. 'created', 'last_updated',
  420. ]
  421. #
  422. # Config templates
  423. #
  424. class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer):
  425. url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail')
  426. data_source = NestedDataSourceSerializer(
  427. required=False
  428. )
  429. data_file = NestedDataFileSerializer(
  430. required=False
  431. )
  432. class Meta:
  433. model = ConfigTemplate
  434. fields = [
  435. 'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source',
  436. 'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated',
  437. ]
  438. #
  439. # Reports
  440. #
  441. class ReportSerializer(serializers.Serializer):
  442. url = serializers.HyperlinkedIdentityField(
  443. view_name='extras-api:report-detail',
  444. lookup_field='full_name',
  445. lookup_url_kwarg='pk'
  446. )
  447. id = serializers.CharField(read_only=True, source="full_name")
  448. module = serializers.CharField(max_length=255)
  449. name = serializers.CharField(max_length=255)
  450. description = serializers.CharField(max_length=255, required=False)
  451. test_methods = serializers.ListField(child=serializers.CharField(max_length=255), read_only=True)
  452. result = NestedJobSerializer()
  453. display = serializers.SerializerMethodField(read_only=True)
  454. @extend_schema_field(serializers.CharField())
  455. def get_display(self, obj):
  456. return f'{obj.name} ({obj.module})'
  457. class ReportDetailSerializer(ReportSerializer):
  458. result = JobSerializer()
  459. class ReportInputSerializer(serializers.Serializer):
  460. schedule_at = serializers.DateTimeField(required=False, allow_null=True)
  461. interval = serializers.IntegerField(required=False, allow_null=True)
  462. def validate_schedule_at(self, value):
  463. if value and not self.context['report'].scheduling_enabled:
  464. raise serializers.ValidationError("Scheduling is not enabled for this report.")
  465. return value
  466. def validate_interval(self, value):
  467. if value and not self.context['report'].scheduling_enabled:
  468. raise serializers.ValidationError("Scheduling is not enabled for this report.")
  469. return value
  470. #
  471. # Scripts
  472. #
  473. class ScriptSerializer(serializers.Serializer):
  474. url = serializers.HyperlinkedIdentityField(
  475. view_name='extras-api:script-detail',
  476. lookup_field='full_name',
  477. lookup_url_kwarg='pk'
  478. )
  479. id = serializers.CharField(read_only=True, source="full_name")
  480. module = serializers.CharField(max_length=255)
  481. name = serializers.CharField(read_only=True)
  482. description = serializers.CharField(read_only=True)
  483. vars = serializers.SerializerMethodField(read_only=True)
  484. result = NestedJobSerializer()
  485. display = serializers.SerializerMethodField(read_only=True)
  486. @extend_schema_field(serializers.JSONField(allow_null=True))
  487. def get_vars(self, instance):
  488. return {
  489. k: v.__class__.__name__ for k, v in instance._get_vars().items()
  490. }
  491. @extend_schema_field(serializers.CharField())
  492. def get_display(self, obj):
  493. return f'{obj.name} ({obj.module})'
  494. class ScriptDetailSerializer(ScriptSerializer):
  495. result = JobSerializer()
  496. class ScriptInputSerializer(serializers.Serializer):
  497. data = serializers.JSONField()
  498. commit = serializers.BooleanField()
  499. schedule_at = serializers.DateTimeField(required=False, allow_null=True)
  500. interval = serializers.IntegerField(required=False, allow_null=True)
  501. def validate_schedule_at(self, value):
  502. if value and not self.context['script'].scheduling_enabled:
  503. raise serializers.ValidationError("Scheduling is not enabled for this script.")
  504. return value
  505. def validate_interval(self, value):
  506. if value and not self.context['script'].scheduling_enabled:
  507. raise serializers.ValidationError("Scheduling is not enabled for this script.")
  508. return value
  509. class ScriptLogMessageSerializer(serializers.Serializer):
  510. status = serializers.SerializerMethodField(read_only=True)
  511. message = serializers.SerializerMethodField(read_only=True)
  512. def get_status(self, instance):
  513. return instance[0]
  514. def get_message(self, instance):
  515. return instance[1]
  516. class ScriptOutputSerializer(serializers.Serializer):
  517. log = ScriptLogMessageSerializer(many=True, read_only=True)
  518. output = serializers.CharField(read_only=True)
  519. #
  520. # Change logging
  521. #
  522. class ObjectChangeSerializer(BaseModelSerializer):
  523. url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail')
  524. user = NestedUserSerializer(
  525. read_only=True
  526. )
  527. action = ChoiceField(
  528. choices=ObjectChangeActionChoices,
  529. read_only=True
  530. )
  531. changed_object_type = ContentTypeField(
  532. read_only=True
  533. )
  534. changed_object = serializers.SerializerMethodField(
  535. read_only=True
  536. )
  537. class Meta:
  538. model = ObjectChange
  539. fields = [
  540. 'id', 'url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type',
  541. 'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
  542. ]
  543. @extend_schema_field(serializers.JSONField(allow_null=True))
  544. def get_changed_object(self, obj):
  545. """
  546. Serialize a nested representation of the changed object.
  547. """
  548. if obj.changed_object is None:
  549. return None
  550. try:
  551. serializer = get_serializer_for_model(obj.changed_object, prefix=NESTED_SERIALIZER_PREFIX)
  552. except SerializerNotFound:
  553. return obj.object_repr
  554. context = {
  555. 'request': self.context['request']
  556. }
  557. data = serializer(obj.changed_object, context=context).data
  558. return data
  559. #
  560. # ContentTypes
  561. #
  562. class ContentTypeSerializer(BaseModelSerializer):
  563. url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail')
  564. class Meta:
  565. model = ContentType
  566. fields = ['id', 'url', 'display', 'app_label', 'model']
  567. #
  568. # User dashboard
  569. #
  570. class DashboardSerializer(serializers.ModelSerializer):
  571. class Meta:
  572. model = Dashboard
  573. fields = ('layout', 'config')