Coverage for src/backend/InvenTree/common/serializers.py: 84%

400 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 17:47 +0000

1"""API serializers for common components.""" 

2 

3from django.contrib.contenttypes.models import ContentType 

4from django.db.models import Count, OuterRef, Subquery 

5from django.urls import reverse 

6from django.utils.translation import gettext_lazy as _ 

7 

8import django_q.models 

9from drf_spectacular.types import OpenApiTypes 

10from drf_spectacular.utils import extend_schema_field 

11from error_report.models import Error 

12from flags.state import flag_state 

13from rest_framework import serializers 

14from rest_framework.exceptions import PermissionDenied 

15from taggit.models import Tag 

16 

17import common.filters 

18import common.models as common_models 

19import common.validators 

20import generic.states.custom 

21from importer.registry import register_importer 

22from InvenTree.helpers import get_objectreference 

23from InvenTree.helpers_model import construct_absolute_url 

24from InvenTree.mixins import DataImportExportSerializerMixin 

25from InvenTree.models import InvenTreeParameterMixin 

26from InvenTree.serializers import ( 

27 ContentTypeField, 

28 FilterableSerializerMixin, 

29 InvenTreeAttachmentSerializerField, 

30 InvenTreeImageSerializerField, 

31 InvenTreeModelSerializer, 

32 InvenTreeTaggitSerializer, 

33 OptionalField, 

34) 

35from plugin import registry as plugin_registry 

36from users.serializers import OwnerSerializer, UserSerializer 

37 

38 

39@extend_schema_field(OpenApiTypes.STR) 

40class SettingsValueField(serializers.Field): 

41 """Custom serializer field for a settings value.""" 

42 

43 def get_attribute(self, instance): 

44 """Return the object instance, not the attribute value.""" 

45 return instance 

46 

47 def to_representation(self, instance: common_models.InvenTreeSetting): 

48 """Return the value of the setting. 

49 

50 Protected settings are returned as '***' 

51 """ 

52 if instance.protected: 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true

53 return '***' 

54 else: 

55 value = instance.value 

56 

57 if value is None: 57 ↛ 58line 57 didn't jump to line 58 because the condition on line 57 was never true

58 value = '' 

59 

60 # Attempt to coerce the value to a native type 

61 if instance.is_int(): 

62 value = instance.as_int() 

63 

64 elif instance.is_float(): 64 ↛ 65line 64 didn't jump to line 65 because the condition on line 64 was never true

65 value = instance.as_float() 

66 

67 elif instance.is_bool(): 

68 value = instance.as_bool() 

69 

70 return value 

71 

72 def to_internal_value(self, data) -> str: 

73 """Return the internal value of the setting.""" 

74 if data is None: 

75 return '' 

76 else: 

77 return str(data) 

78 

79 

80class SettingsSerializer(InvenTreeModelSerializer): 

81 """Base serializer for a settings object.""" 

82 

83 key = serializers.CharField(read_only=True) 

84 

85 name = serializers.CharField(read_only=True) 

86 

87 description = serializers.CharField(read_only=True) 

88 

89 type = serializers.CharField(source='setting_type', read_only=True) 

90 

91 choices = serializers.SerializerMethodField() 

92 

93 def get_choices(self, obj) -> list: 

94 """Returns the choices available for a given item.""" 

95 results = [] 

96 

97 choices = obj.choices() 

98 

99 if choices: 

100 for choice in choices: 

101 results.append({'value': choice[0], 'display_name': choice[1]}) 

102 

103 return results 

104 

105 model_name = serializers.CharField(read_only=True, allow_null=True) 

106 

107 model_filters = serializers.DictField(read_only=True) 

108 

109 api_url = serializers.CharField(read_only=True, allow_null=True) 

110 

111 value = SettingsValueField(allow_null=True) 

112 

113 def validate_value(self, value): 

114 """Validate the value of the setting.""" 

115 if value is None: 

116 return '' 

117 return str(value) 

118 

119 units = serializers.CharField(read_only=True) 

120 

121 required = serializers.BooleanField(read_only=True) 

122 

123 typ = serializers.CharField(read_only=True) 

124 

125 confirm = serializers.BooleanField( 

126 read_only=True, 

127 help_text=_('Indicates if changing this setting requires confirmation'), 

128 ) 

129 

130 confirm_text = serializers.CharField(read_only=True) 

131 

132 def is_valid(self, *, raise_exception=False): 

133 """Validate the setting, including confirmation if required.""" 

134 ret = super().is_valid(raise_exception=raise_exception) 

135 # Check if confirmation was provided if required 

136 if self.instance.confirm(): 

137 req_data = self.context['request'].data 

138 if not 'manual_confirm' in req_data or not req_data['manual_confirm']: 

139 raise serializers.ValidationError({ 

140 'manual_confirm': _( 

141 'This setting requires confirmation before changing. Please confirm the change.' 

142 ) 

143 }) 

144 return ret 

145 

146 

147class GlobalSettingsSerializer(SettingsSerializer): 

148 """Serializer for the InvenTreeSetting model.""" 

149 

150 class Meta: 

151 """Meta options for GlobalSettingsSerializer.""" 

152 

153 model = common_models.InvenTreeSetting 

154 fields = [ 

155 'pk', 

156 'key', 

157 'value', 

158 'name', 

159 'description', 

160 'type', 

161 'units', 

162 'choices', 

163 'model_name', 

164 'api_url', 

165 'typ', 

166 'read_only', 

167 'confirm', 

168 'confirm_text', 

169 ] 

170 

171 read_only = serializers.SerializerMethodField( 

172 read_only=True, 

173 help_text=_( 

174 'Indicates if the setting is overridden by an environment variable' 

175 ), 

176 label=_('Override'), 

177 ) 

178 

179 def get_read_only(self, obj) -> bool: 

180 """Return True if the setting 'read_only' (cannot be edited). 

181 

182 A setting may be "read-only" if: 

183 

184 - It is overridden by an environment variable. 

185 """ 

186 from common.settings import global_setting_overrides 

187 

188 overrides = global_setting_overrides() 

189 return obj.key in overrides 

190 

191 

192class UserSettingsSerializer(SettingsSerializer): 

193 """Serializer for the InvenTreeUserSetting model.""" 

194 

195 class Meta: 

196 """Meta options for UserSettingsSerializer.""" 

197 

198 model = common_models.InvenTreeUserSetting 

199 fields = [ 

200 'pk', 

201 'key', 

202 'value', 

203 'name', 

204 'description', 

205 'user', 

206 'type', 

207 'units', 

208 'choices', 

209 'model_name', 

210 'api_url', 

211 'typ', 

212 'confirm', 

213 'confirm_text', 

214 ] 

215 

216 user = serializers.PrimaryKeyRelatedField(read_only=True) 

217 

218 

219class CurrencyExchangeSerializer(serializers.Serializer): 

220 """Serializer for a Currency Exchange request. 

221 

222 It's only purpose is describing the results correctly in the API schema right now. 

223 """ 

224 

225 base_currency = serializers.CharField(read_only=True) 

226 exchange_rates = serializers.DictField(child=serializers.FloatField()) 

227 updated = serializers.DateTimeField(read_only=True) 

228 

229 

230class GenericReferencedSettingSerializer(SettingsSerializer): 

231 """Serializer for a GenericReferencedSetting model. 

232 

233 Args: 

234 MODEL: model class for the serializer 

235 EXTRA_FIELDS: fields that need to be appended to the serializer 

236 field must also be defined in the custom class 

237 """ 

238 

239 MODEL = None 

240 EXTRA_FIELDS = None 

241 

242 def __init__(self, *args, **kwargs): 

243 """Init overrides the Meta class to make it dynamic.""" 

244 

245 class CustomMeta: 

246 """Scaffold for custom Meta class.""" 

247 

248 fields = [ 

249 'pk', 

250 'key', 

251 'value', 

252 'name', 

253 'description', 

254 'type', 

255 'choices', 

256 'model_name', 

257 'model_filters', 

258 'api_url', 

259 'typ', 

260 'units', 

261 'required', 

262 'confirm', 

263 'confirm_text', 

264 ] 

265 

266 # set Meta class 

267 self.Meta = CustomMeta 

268 self.Meta.model = self.MODEL 

269 # extend the fields 

270 self.Meta.fields.extend(self.EXTRA_FIELDS) 

271 

272 # resume operations 

273 super().__init__(*args, **kwargs) 

274 

275 def validate_value(self, value): 

276 """Validate the value of the setting.""" 

277 if value is None: 

278 return '' 

279 return str(value) 

280 

281 

282class NotificationMessageSerializer(InvenTreeModelSerializer): 

283 """Serializer for the InvenTreeUserSetting model.""" 

284 

285 class Meta: 

286 """Meta options for NotificationMessageSerializer.""" 

287 

288 model = common_models.NotificationMessage 

289 fields = [ 

290 'pk', 

291 'target', 

292 'source', 

293 'user', 

294 'category', 

295 'name', 

296 'message', 

297 'creation', 

298 'age', 

299 'age_human', 

300 'read', 

301 ] 

302 

303 read_only_fields = [ 

304 'category', 

305 'name', 

306 'message', 

307 'creation', 

308 'age', 

309 'age_human', 

310 ] 

311 

312 target = serializers.SerializerMethodField(read_only=True) 

313 source = serializers.SerializerMethodField(read_only=True) 

314 user = serializers.PrimaryKeyRelatedField(read_only=True) 

315 read = serializers.BooleanField() 

316 

317 def get_target(self, obj) -> dict: 

318 """Function to resolve generic object reference to target.""" 

319 target = get_objectreference(obj, 'target_content_type', 'target_object_id') 

320 

321 if target and 'link' not in target: 321 ↛ 342line 321 didn't jump to line 342 because the condition on line 321 was always true

322 # Check if object has an absolute_url function 

323 if hasattr(obj.target_object, 'get_absolute_url'): 323 ↛ 324line 323 didn't jump to line 324 because the condition on line 323 was never true

324 target['link'] = obj.target_object.get_absolute_url() 

325 else: 

326 # check if user is staff - link to admin 

327 request = self.context.get('request') 

328 if request and request.user and request.user.is_staff: 328 ↛ 342line 328 didn't jump to line 342 because the condition on line 328 was always true

329 meta = obj.target_object._meta 

330 

331 try: 

332 target['link'] = construct_absolute_url( 

333 reverse( 

334 f'admin:{meta.db_table}_change', 

335 kwargs={'object_id': obj.target_object_id}, 

336 ) 

337 ) 

338 except Exception: 

339 # Do not crash if the reverse lookup fails 

340 pass 

341 

342 return target 

343 

344 def get_source(self, obj) -> dict: 

345 """Function to resolve generic object reference to source.""" 

346 return get_objectreference(obj, 'source_content_type', 'source_object_id') 

347 

348 

349class NewsFeedEntrySerializer(InvenTreeModelSerializer): 

350 """Serializer for the NewsFeedEntry model.""" 

351 

352 class Meta: 

353 """Meta options for NewsFeedEntrySerializer.""" 

354 

355 model = common_models.NewsFeedEntry 

356 fields = [ 

357 'pk', 

358 'feed_id', 

359 'title', 

360 'link', 

361 'published', 

362 'author', 

363 'summary', 

364 'read', 

365 ] 

366 

367 read = serializers.BooleanField() 

368 

369 

370class ConfigSerializer(serializers.Serializer): 

371 """Serializer for the InvenTree configuration. 

372 

373 This is a read-only serializer. 

374 """ 

375 

376 key = serializers.CharField(read_only=True) 

377 env_var = serializers.CharField(read_only=True, allow_null=True) 

378 config_key = serializers.CharField(read_only=True, allow_null=True) 

379 source = serializers.CharField(read_only=True) 

380 accessed = serializers.DateTimeField(read_only=True) 

381 

382 def to_representation(self, instance): 

383 """Return the configuration data as a dictionary.""" 

384 if not isinstance(instance, str): 

385 instance = list(instance.keys())[0] 

386 

387 data = {'key': instance} 

388 

389 for k, v in self.instance.get(instance, {}).items(): 

390 if k == 'default_value': 

391 # Skip sensitive default values 

392 continue 

393 data[k] = v 

394 

395 return data 

396 

397 

398class NotesImageSerializer(InvenTreeModelSerializer): 

399 """Serializer for the NotesImage model.""" 

400 

401 class Meta: 

402 """Meta options for NotesImageSerializer.""" 

403 

404 model = common_models.NotesImage 

405 fields = ['pk', 'image', 'user', 'date', 'model_type', 'model_id'] 

406 

407 read_only_fields = ['date', 'user'] 

408 

409 image = InvenTreeImageSerializerField(required=True) 

410 

411 

412@register_importer() 

413class ProjectCodeSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer): 

414 """Serializer for the ProjectCode model.""" 

415 

416 class Meta: 

417 """Meta options for ProjectCodeSerializer.""" 

418 

419 model = common_models.ProjectCode 

420 fields = ['pk', 'code', 'description', 'responsible', 'responsible_detail'] 

421 

422 responsible_detail = OwnerSerializer( 

423 source='responsible', read_only=True, allow_null=True 

424 ) 

425 

426 

427@register_importer() 

428class TagSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer): 

429 """Serializer for the Tag model.""" 

430 

431 class Meta: 

432 """Meta options for TagSerializer.""" 

433 

434 model = Tag 

435 fields = ['pk', 'name', 'slug'] 

436 read_only_fields = ['pk', 'slug'] 

437 

438 def validate(self, data): 

439 """Slugify the received name to generate the slug.""" 

440 from django.utils.text import slugify 

441 

442 name = data.get('name', None) 

443 

444 if name is not None: 

445 data['slug'] = slugify(name) 

446 

447 return data 

448 

449 

450@register_importer() 

451class CustomStateSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer): 

452 """Serializer for the custom state model.""" 

453 

454 class Meta: 

455 """Meta options for CustomStateSerializer.""" 

456 

457 model = common_models.InvenTreeCustomUserStateModel 

458 fields = [ 

459 'pk', 

460 'key', 

461 'name', 

462 'label', 

463 'color', 

464 'logical_key', 

465 'model', 

466 'model_name', 

467 'reference_status', 

468 ] 

469 

470 model_name = serializers.CharField(read_only=True, source='model.name') 

471 

472 reference_status = serializers.ChoiceField( 

473 choices=generic.states.custom.state_reference_mappings() 

474 ) 

475 

476 

477class FlagSerializer(serializers.Serializer): 

478 """Serializer for feature flags.""" 

479 

480 key = serializers.CharField(read_only=True) 

481 state = serializers.CharField(read_only=True) 

482 conditions = serializers.ListField( 

483 child=serializers.DictField(), read_only=True, allow_null=True 

484 ) 

485 

486 def to_representation(self, instance): 

487 """Return the configuration data as a dictionary.""" 

488 request = self.context.get('request') 

489 if not isinstance(instance, str): 

490 instance = list(instance.keys())[0] 

491 data = {'key': instance, 'state': flag_state(instance, request=request)} 

492 

493 if request and request.user.is_superuser: 

494 data['conditions'] = self.instance.get(instance) 

495 

496 return data 

497 

498 

499class ContentTypeSerializer(serializers.Serializer): 

500 """Serializer for ContentType models.""" 

501 

502 pk = serializers.IntegerField(read_only=True) 

503 app_label = serializers.CharField(read_only=True) 

504 model = serializers.CharField(read_only=True) 

505 app_labeled_name = serializers.CharField(read_only=True) 

506 is_plugin = serializers.SerializerMethodField('get_is_plugin', read_only=True) 

507 

508 class Meta: 

509 """Meta options for ContentTypeSerializer.""" 

510 

511 model = ContentType 

512 fields = ['pk', 'app_label', 'model', 'app_labeled_name', 'is_plugin'] 

513 

514 def get_is_plugin(self, obj) -> bool: 

515 """Return True if the model is a plugin model.""" 

516 return obj.app_label in plugin_registry.installed_apps 

517 

518 

519@register_importer() 

520class CustomUnitSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer): 

521 """DRF serializer for CustomUnit model.""" 

522 

523 class Meta: 

524 """Meta options for CustomUnitSerializer.""" 

525 

526 model = common_models.CustomUnit 

527 fields = ['pk', 'name', 'symbol', 'definition'] 

528 

529 

530class AllUnitListResponseSerializer(serializers.Serializer): 

531 """Serializer for the AllUnitList.""" 

532 

533 class Unit(serializers.Serializer): 

534 """Serializer for the AllUnitListResponseSerializer.""" 

535 

536 name = serializers.CharField() 

537 is_alias = serializers.BooleanField() 

538 compatible_units = serializers.ListField(child=serializers.CharField()) 

539 isdimensionless = serializers.BooleanField() 

540 

541 default_system = serializers.CharField() 

542 available_systems = serializers.ListField(child=serializers.CharField()) 

543 available_units = serializers.DictField(child=Unit()) 

544 

545 

546class ErrorMessageSerializer(InvenTreeModelSerializer): 

547 """DRF serializer for server error messages.""" 

548 

549 class Meta: 

550 """Metaclass options for ErrorMessageSerializer.""" 

551 

552 model = Error 

553 

554 fields = ['when', 'info', 'data', 'path', 'pk'] 

555 

556 read_only_fields = ['when', 'info', 'data', 'path', 'pk'] 

557 

558 

559class TaskDetailSerializer(serializers.Serializer): 

560 """Serializer for a background task detail.""" 

561 

562 task_id = serializers.CharField(read_only=True) 

563 exists = serializers.BooleanField(read_only=True) 

564 pending = serializers.BooleanField(read_only=True) 

565 complete = serializers.BooleanField(read_only=True) 

566 success = serializers.BooleanField(read_only=True) 

567 http_status = serializers.IntegerField(read_only=True) 

568 

569 @classmethod 

570 def from_task(cls, task_id: str | bool | None) -> 'TaskDetailSerializer': 

571 """Create a TaskDetailSerializer instance from a django_q Task. 

572 

573 Arguments: 

574 task_id: The ID of the task to retrieve details for. 

575 

576 Returns: 

577 An instance of TaskDetailSerializer with the task details. 

578 

579 Notes: 

580 - If the provided task_id is None, the task has not been run, or has errored out 

581 - If the provided task_id is a boolean, the task has been run synchronously, and the boolean value indicates success or failure 

582 - If the provided task_id is a string, the task has been offloaded to the background worker, and the details can be from the database 

583 

584 """ 

585 from InvenTree.tasks import get_queued_task 

586 

587 if task_id is None or type(task_id) is bool: 

588 # If the task_id is a boolean, the task has been run synchronously 

589 return cls({ 

590 'task_id': '', 

591 'exists': False, 

592 'pending': False, 

593 'complete': task_id is not None, 

594 'success': False if task_id is None else bool(task_id), 

595 'http_status': 404 if task_id is None else 200, 

596 }) 

597 

598 # A non-boolean result indicates that the task has been offloaded to the background worker 

599 success = django_q.models.Success.objects.filter(id=task_id).first() 

600 failure = django_q.models.Failure.objects.filter(id=task_id).first() 

601 task = ( 

602 success 

603 or failure 

604 or django_q.models.Task.objects.filter(id=task_id).first() 

605 ) 

606 queued = False 

607 

608 exists = bool(success or failure or task) 

609 

610 if not exists: 610 ↛ 614line 610 didn't jump to line 614 because the condition on line 610 was always true

611 # If the task has not been started yet, it may be present in the queue 

612 queued = bool(get_queued_task(task_id)) 

613 

614 complete = bool(success) or bool(failure) 

615 

616 # Determine the http_status code for the task 

617 # - 200: Task exists and has been completed 

618 # - 404: Task does not exist 

619 http_status = 200 if exists or queued else 404 

620 

621 return cls({ 

622 'task_id': task_id, 

623 'exists': exists or queued, 

624 'pending': queued, 

625 'complete': complete, 

626 'success': bool(success), 

627 'http_status': http_status, 

628 }) 

629 

630 

631class TaskOverviewSerializer(serializers.Serializer): 

632 """Serializer for background task overview.""" 

633 

634 is_running = serializers.BooleanField( 

635 label=_('Is Running'), 

636 help_text='Boolean value to indicate if the background worker process is running.', 

637 read_only=True, 

638 ) 

639 

640 pending_tasks = serializers.IntegerField( 

641 label=_('Pending Tasks'), 

642 help_text='Number of active background tasks', 

643 read_only=True, 

644 ) 

645 

646 scheduled_tasks = serializers.IntegerField( 

647 label=_('Scheduled Tasks'), 

648 help_text='Number of scheduled background tasks', 

649 read_only=True, 

650 ) 

651 

652 failed_tasks = serializers.IntegerField( 

653 label=_('Failed Tasks'), 

654 help_text='Number of failed background tasks', 

655 read_only=True, 

656 ) 

657 

658 

659class PendingTaskSerializer(InvenTreeModelSerializer): 

660 """Serializer for an individual pending task object.""" 

661 

662 class Meta: 

663 """Metaclass options for the serializer.""" 

664 

665 model = django_q.models.OrmQ 

666 fields = ['pk', 'key', 'lock', 'task_id', 'name', 'func', 'args', 'kwargs'] 

667 

668 task_id = serializers.CharField(label=_('Task ID'), help_text=_('Unique task ID')) 

669 

670 lock = serializers.DateTimeField(label=_('Lock'), help_text=_('Lock time')) 

671 

672 name = serializers.CharField(label=_('Name'), help_text=_('Task name')) 

673 

674 func = serializers.CharField(label=_('Function'), help_text=_('Function name')) 

675 

676 args = serializers.CharField(label=_('Arguments'), help_text=_('Task arguments')) 

677 

678 kwargs = serializers.CharField( 

679 label=_('Keyword Arguments'), help_text=_('Task keyword arguments') 

680 ) 

681 

682 

683class ScheduledTaskSerializer(InvenTreeModelSerializer): 

684 """Serializer for an individual scheduled task object.""" 

685 

686 class Meta: 

687 """Metaclass options for the serializer.""" 

688 

689 model = django_q.models.Schedule 

690 fields = [ 

691 'pk', 

692 'name', 

693 'func', 

694 'args', 

695 'kwargs', 

696 'schedule_type', 

697 'repeats', 

698 'last_run', 

699 'next_run', 

700 'success', 

701 'task', 

702 ] 

703 

704 last_run = serializers.DateTimeField() 

705 success = serializers.BooleanField() 

706 

707 @staticmethod 

708 def annotate_queryset(queryset): 

709 """Add custom annotations to the queryset. 

710 

711 - last_run: The last time the task was run 

712 - success: The outcome status of the last run 

713 """ 

714 task = django_q.models.Task.objects.filter(id=OuterRef('task')) 

715 

716 queryset = queryset.annotate( 

717 last_run=Subquery(task.values('started')[:1]), 

718 success=Subquery(task.values('success')[:1]), 

719 ) 

720 

721 return queryset 

722 

723 

724class FailedTaskSerializer(InvenTreeModelSerializer): 

725 """Serializer for an individual failed task object.""" 

726 

727 class Meta: 

728 """Metaclass options for the serializer.""" 

729 

730 model = django_q.models.Failure 

731 fields = [ 

732 'pk', 

733 'name', 

734 'func', 

735 'args', 

736 'kwargs', 

737 'started', 

738 'stopped', 

739 'attempt_count', 

740 'result', 

741 ] 

742 

743 pk = serializers.CharField(source='id', read_only=True) 

744 

745 result = serializers.CharField() 

746 

747 

748class AttachmentSerializer( 

749 FilterableSerializerMixin, InvenTreeTaggitSerializer, InvenTreeModelSerializer 

750): 

751 """Serializer class for the Attachment model.""" 

752 

753 class Meta: 

754 """Serializer metaclass.""" 

755 

756 model = common_models.Attachment 

757 fields = [ 

758 'pk', 

759 'attachment', 

760 'thumbnail', 

761 'filename', 

762 'link', 

763 'comment', 

764 'is_image', 

765 'upload_date', 

766 'upload_user', 

767 'user_detail', 

768 'file_size', 

769 'model_type', 

770 'model_id', 

771 'tags', 

772 ] 

773 

774 read_only_fields = [ 

775 'pk', 

776 'file_size', 

777 'upload_date', 

778 'upload_user', 

779 'filename', 

780 'is_image', 

781 ] 

782 

783 def __init__(self, *args, **kwargs): 

784 """Override the model_type field to provide dynamic choices.""" 

785 super().__init__(*args, **kwargs) 

786 

787 if len(self.fields['model_type'].choices) == 0: 787 ↛ exitline 787 didn't return from function '__init__' because the condition on line 787 was always true

788 self.fields[ 

789 'model_type' 

790 ].choices = common.validators.attachment_model_options() 

791 

792 tags = common.filters.enable_tags_filter() 

793 

794 user_detail = UserSerializer(source='upload_user', read_only=True, many=False) 

795 

796 attachment = InvenTreeAttachmentSerializerField(required=False, allow_null=True) 

797 

798 thumbnail = InvenTreeImageSerializerField(read_only=True, allow_null=True) 

799 

800 # The 'filename' field must be present in the serializer 

801 filename = serializers.CharField( 

802 label=_('Filename'), required=False, source='basename', allow_blank=False 

803 ) 

804 

805 upload_date = serializers.DateField(read_only=True) 

806 

807 # Note: The choices are overridden at run-time on class initialization 

808 model_type = serializers.ChoiceField( 

809 label=_('Model Type'), 

810 choices=common.validators.attachment_model_options(), 

811 required=True, 

812 allow_blank=False, 

813 allow_null=False, 

814 ) 

815 

816 def save(self, **kwargs): 

817 """Override the save method to handle the model_type field.""" 

818 from InvenTree.models import InvenTreeAttachmentMixin 

819 from users.permissions import check_user_permission 

820 

821 model_type = self.validated_data.get('model_type', None) 

822 

823 if model_type is None and self.instance: 823 ↛ 824line 823 didn't jump to line 824 because the condition on line 823 was never true

824 model_type = self.instance.model_type 

825 

826 # Ensure that the user has permission to attach files to the specified model 

827 user = self.context.get('request').user 

828 

829 target_model_class = common.validators.attachment_model_class_from_label( 

830 model_type 

831 ) 

832 

833 if not issubclass(target_model_class, InvenTreeAttachmentMixin): 833 ↛ 834line 833 didn't jump to line 834 because the condition on line 833 was never true

834 raise PermissionDenied(_('Invalid model type specified for attachment')) 

835 

836 permission_error_msg = _( 

837 'User does not have permission to create or edit attachments for this model' 

838 ) 

839 

840 if not check_user_permission(user, target_model_class, 'change'): 840 ↛ 841line 840 didn't jump to line 841 because the condition on line 840 was never true

841 raise PermissionDenied(permission_error_msg) 

842 

843 # Check that the user has the required permissions to attach files to the target model 

844 if not target_model_class.check_related_permission('change', user): 844 ↛ 845line 844 didn't jump to line 845 because the condition on line 844 was never true

845 raise PermissionDenied(permission_error_msg) 

846 

847 return super().save(**kwargs) 

848 

849 

850@register_importer() 

851class ParameterTemplateSerializer( 

852 DataImportExportSerializerMixin, InvenTreeModelSerializer 

853): 

854 """Serializer for the ParameterTemplate model.""" 

855 

856 class Meta: 

857 """Meta options for ParameterTemplateSerializer.""" 

858 

859 model = common_models.ParameterTemplate 

860 fields = [ 

861 'pk', 

862 'name', 

863 'units', 

864 'description', 

865 'model_type', 

866 'checkbox', 

867 'choices', 

868 'selectionlist', 

869 'enabled', 

870 ] 

871 

872 # Note: The choices are overridden at run-time on class initialization 

873 model_type = ContentTypeField( 

874 mixin_class=InvenTreeParameterMixin, 

875 choices=common.validators.parameter_template_model_options, 

876 label=_('Model Type'), 

877 default='', 

878 required=False, 

879 allow_null=True, 

880 ) 

881 

882 def validate_model_type(self, model_type): 

883 """Convert an empty string to None for the model_type field.""" 

884 return model_type or None 

885 

886 

887@register_importer() 

888class ParameterSerializer( 

889 FilterableSerializerMixin, DataImportExportSerializerMixin, InvenTreeModelSerializer 

890): 

891 """Serializer for the Parameter model.""" 

892 

893 class Meta: 

894 """Meta options for ParameterSerializer.""" 

895 

896 model = common_models.Parameter 

897 fields = [ 

898 'pk', 

899 'template', 

900 'model_type', 

901 'model_id', 

902 'data', 

903 'data_numeric', 

904 'note', 

905 'updated', 

906 'updated_by', 

907 # Optional fields 

908 'template_detail', 

909 'updated_by_detail', 

910 ] 

911 

912 read_only_fields = ['updated', 'updated_by'] 

913 

914 def save(self, **kwargs): 

915 """Save the Parameter instance.""" 

916 from InvenTree.models import InvenTreeParameterMixin 

917 from users.permissions import check_user_permission 

918 

919 model_type = self.validated_data.get('model_type', None) 

920 

921 if model_type is None and self.instance: 

922 model_type = self.instance.model_type 

923 

924 # Ensure that the user has permission to modify parameters for the specified model 

925 user = self.context.get('request').user 

926 

927 target_model_class = model_type.model_class() 

928 

929 if not issubclass(target_model_class, InvenTreeParameterMixin): 

930 raise PermissionDenied(_('Invalid model type specified for parameter')) 

931 

932 permission_error_msg = _( 

933 'User does not have permission to create or edit parameters for this model' 

934 ) 

935 

936 if not check_user_permission(user, target_model_class, 'change'): 

937 raise PermissionDenied(permission_error_msg) 

938 

939 if not target_model_class.check_related_permission('change', user): 

940 raise PermissionDenied(permission_error_msg) 

941 

942 instance = super().save(**kwargs) 

943 instance.updated_by = user 

944 instance.save() 

945 

946 return instance 

947 

948 # Note: The choices are overridden at run-time on class initialization 

949 model_type = ContentTypeField( 

950 mixin_class=InvenTreeParameterMixin, 

951 choices=common.validators.parameter_model_options, 

952 label=_('Model Type'), 

953 default='', 

954 allow_null=False, 

955 ) 

956 

957 updated_by_detail = OptionalField( 

958 serializer_class=UserSerializer, 

959 serializer_kwargs={ 

960 'source': 'updated_by', 

961 'read_only': True, 

962 'allow_null': True, 

963 'many': False, 

964 }, 

965 default_include=True, 

966 prefetch_fields=['updated_by'], 

967 ) 

968 

969 template_detail = OptionalField( 

970 serializer_class=ParameterTemplateSerializer, 

971 serializer_kwargs={'source': 'template', 'read_only': True, 'many': False}, 

972 default_include=True, 

973 prefetch_fields=['template', 'template__model_type'], 

974 ) 

975 

976 

977class IconSerializer(serializers.Serializer): 

978 """Serializer for an icon.""" 

979 

980 name = serializers.CharField() 

981 category = serializers.CharField() 

982 tags = serializers.ListField(child=serializers.CharField()) 

983 variants = serializers.DictField(child=serializers.CharField()) 

984 

985 

986class IconPackageSerializer(serializers.Serializer): 

987 """Serializer for a list of icons.""" 

988 

989 name = serializers.CharField() 

990 prefix = serializers.CharField() 

991 fonts = serializers.DictField(child=serializers.CharField()) 

992 icons = serializers.DictField(child=IconSerializer()) 

993 

994 

995class SelectionEntrySerializer(InvenTreeModelSerializer): 

996 """Serializer for a selection entry.""" 

997 

998 class Meta: 

999 """Meta options for SelectionEntrySerializer.""" 

1000 

1001 model = common_models.SelectionListEntry 

1002 fields = '__all__' 

1003 

1004 def validate(self, attrs): 

1005 """Ensure that the selection list is not locked.""" 

1006 ret = super().validate(attrs) 

1007 list_obj = attrs.get('list') or (self.instance and self.instance.list) 

1008 if list_obj and list_obj.locked: 1008 ↛ 1009line 1008 didn't jump to line 1009 because the condition on line 1008 was never true

1009 raise serializers.ValidationError({'list': _('Selection list is locked')}) 

1010 return ret 

1011 

1012 

1013class SelectionListSerializer(FilterableSerializerMixin, InvenTreeModelSerializer): 

1014 """Serializer for a selection list.""" 

1015 

1016 _choices_validated: dict = {} 

1017 _choices_provided: bool = False 

1018 

1019 class Meta: 

1020 """Meta options for SelectionListSerializer.""" 

1021 

1022 model = common_models.SelectionList 

1023 fields = [ 

1024 'pk', 

1025 'name', 

1026 'description', 

1027 'active', 

1028 'locked', 

1029 'source_plugin', 

1030 'source_string', 

1031 'default', 

1032 'created', 

1033 'last_updated', 

1034 'entry_count', 

1035 'choices', 

1036 ] 

1037 

1038 default = SelectionEntrySerializer(read_only=True, allow_null=True, many=False) 

1039 entry_count = serializers.IntegerField(read_only=True) 

1040 

1041 choices = OptionalField( 

1042 serializer_class=SelectionEntrySerializer, 

1043 serializer_kwargs={'source': 'entries', 'many': True, 'read_only': True}, 

1044 prefetch_fields=['entries'], 

1045 default_include=True, 

1046 ) 

1047 

1048 @staticmethod 

1049 def annotate_queryset(queryset): 

1050 """Add count of entries for each selection list.""" 

1051 return queryset.annotate(entry_count=Count('entries')) 

1052 

1053 def validate(self, attrs): 

1054 """Ensure that the selection list is not locked.""" 

1055 ret = super().validate(attrs) 

1056 if self.instance and self.instance.locked: 1056 ↛ 1057line 1056 didn't jump to line 1057 because the condition on line 1056 was never true

1057 raise serializers.ValidationError({'locked': _('Selection list is locked')}) 

1058 return ret 

1059 

1060 

1061class DataOutputSerializer(InvenTreeModelSerializer): 

1062 """Serializer for the DataOutput model.""" 

1063 

1064 class Meta: 

1065 """Meta options for DataOutputSerializer.""" 

1066 

1067 model = common_models.DataOutput 

1068 fields = [ 

1069 'pk', 

1070 'created', 

1071 'user', 

1072 'user_detail', 

1073 'total', 

1074 'progress', 

1075 'complete', 

1076 'output_type', 

1077 'template_name', 

1078 'plugin', 

1079 'output', 

1080 'errors', 

1081 ] 

1082 

1083 user_detail = UserSerializer(source='user', read_only=True, many=False) 

1084 

1085 output = InvenTreeAttachmentSerializerField(allow_null=True, read_only=True) 

1086 

1087 

1088class EmailMessageSerializer(InvenTreeModelSerializer): 

1089 """Serializer for the EmailMessage model.""" 

1090 

1091 class Meta: 

1092 """Meta options for EmailMessageSerializer.""" 

1093 

1094 model = common_models.EmailMessage 

1095 fields = [ 

1096 'pk', 

1097 'global_id', 

1098 'message_id_key', 

1099 'thread_id_key', 

1100 'thread', 

1101 'subject', 

1102 'body', 

1103 'to', 

1104 'sender', 

1105 'status', 

1106 'timestamp', 

1107 'headers', 

1108 'full_message', 

1109 'direction', 

1110 'priority', 

1111 'error_code', 

1112 'error_message', 

1113 'error_timestamp', 

1114 'delivery_options', 

1115 ] 

1116 

1117 

1118class TestEmailSerializer(serializers.Serializer): 

1119 """Serializer to send a test email.""" 

1120 

1121 class Meta: 

1122 """Meta options for TestEmailSerializer.""" 

1123 

1124 fields = ['email'] 

1125 

1126 email = serializers.EmailField(required=True)