# Copyright (c) 2016 Ansible, Inc. # All Rights Reserved. from collections import OrderedDict from uuid import UUID # Django from django.core.exceptions import PermissionDenied from django.db.models.fields import PositiveIntegerField, BooleanField from django.db.models.fields.related import ForeignKey from django.http import Http404 from django.utils.encoding import force_text, smart_text from django.utils.translation import ugettext_lazy as _ # Django REST Framework from rest_framework import exceptions from rest_framework import metadata from rest_framework import serializers from rest_framework.relations import RelatedField, ManyRelatedField from rest_framework.fields import JSONField as DRFJSONField from rest_framework.request import clone_request # AWX from awx.api.fields import ChoiceNullField from awx.main.fields import JSONField, ImplicitRoleField from awx.main.models import NotificationTemplate from awx.main.scheduler.kubernetes import PodManager class Metadata(metadata.SimpleMetadata): def get_field_info(self, field): field_info = OrderedDict() field_info['type'] = self.label_lookup[field] field_info['required'] = getattr(field, 'required', False) text_attrs = [ 'read_only', 'label', 'help_text', 'min_length', 'max_length', 'min_value', 'max_value', 'category', 'category_slug', 'defined_in_file', 'unit', ] for attr in text_attrs: value = getattr(field, attr, None) if value is not None and value != '': field_info[attr] = force_text(value, strings_only=True) placeholder = getattr(field, 'placeholder', serializers.empty) if placeholder is not serializers.empty: field_info['placeholder'] = placeholder serializer = getattr(field, 'parent', None) if serializer and hasattr(serializer, 'Meta') and hasattr(serializer.Meta, 'model'): # Update help text for common fields. field_help_text = { 'id': _('Database ID for this {}.'), 'name': _('Name of this {}.'), 'description': _('Optional description of this {}.'), 'type': _('Data type for this {}.'), 'url': _('URL for this {}.'), 'related': _('Data structure with URLs of related resources.'), 'summary_fields': _('Data structure with name/description for related resources. ' 'The output for some objects may be limited for performance reasons.'), 'created': _('Timestamp when this {} was created.'), 'modified': _('Timestamp when this {} was last modified.'), } if field.field_name in field_help_text: opts = serializer.Meta.model._meta.concrete_model._meta verbose_name = smart_text(opts.verbose_name) field_info['help_text'] = field_help_text[field.field_name].format(verbose_name) if field.field_name == 'type': field_info['filterable'] = True else: for model_field in serializer.Meta.model._meta.fields: if field.field_name == model_field.name: if getattr(model_field, '__accepts_json__', None): field_info['type'] = 'json' field_info['filterable'] = True break else: field_info['filterable'] = False # Indicate if a field has a default value. # FIXME: Still isn't showing all default values? try: default = field.get_default() if type(default) is UUID: default = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' if field.field_name == 'TOWER_URL_BASE' and default == 'https://towerhost': default = '{}://{}'.format(self.request.scheme, self.request.get_host()) field_info['default'] = default except serializers.SkipField: pass if getattr(field, 'child', None): field_info['child'] = self.get_field_info(field.child) elif getattr(field, 'fields', None): field_info['children'] = self.get_serializer_info(field) if not isinstance(field, (RelatedField, ManyRelatedField)) and hasattr(field, 'choices'): choices = [ (choice_value, choice_name) for choice_value, choice_name in field.choices.items() ] if not any(choice in ('', None) for choice, _ in choices): if field.allow_blank: choices = [("", "---------")] + choices if field.allow_null and not isinstance(field, ChoiceNullField): choices = [(None, "---------")] + choices field_info['choices'] = choices # Indicate if a field is write-only. if getattr(field, 'write_only', False): field_info['write_only'] = True # Special handling of notification configuration where the required properties # are conditional on the type selected. if field.field_name == 'notification_configuration': for (notification_type_name, notification_tr_name, notification_type_class) in NotificationTemplate.NOTIFICATION_TYPES: field_info[notification_type_name] = notification_type_class.init_parameters # Special handling of notification messages where the required properties # are conditional on the type selected. try: view_model = field.context['view'].model except (AttributeError, KeyError): view_model = None if view_model == NotificationTemplate and field.field_name == 'messages': for (notification_type_name, notification_tr_name, notification_type_class) in NotificationTemplate.NOTIFICATION_TYPES: field_info[notification_type_name] = notification_type_class.default_messages # Update type of fields returned... model_field = None if serializer and hasattr(serializer, 'Meta') and hasattr(serializer.Meta, 'model'): try: model_field = serializer.Meta.model._meta.get_field(field.field_name) except Exception: pass if field.field_name == 'type': field_info['type'] = 'choice' elif field.field_name in ('url', 'custom_virtualenv', 'token'): field_info['type'] = 'string' elif field.field_name in ('related', 'summary_fields'): field_info['type'] = 'object' elif isinstance(field, PositiveIntegerField): field_info['type'] = 'integer' elif field.field_name in ('created', 'modified'): field_info['type'] = 'datetime' elif ( RelatedField in field.__class__.__bases__ or isinstance(model_field, ForeignKey) ): field_info['type'] = 'id' elif ( isinstance(field, JSONField) or isinstance(model_field, JSONField) or isinstance(field, DRFJSONField) or isinstance(getattr(field, 'model_field', None), JSONField) or field.field_name == 'credential_passwords' ): field_info['type'] = 'json' elif ( isinstance(field, ManyRelatedField) and field.field_name == 'credentials' # launch-time credentials ): field_info['type'] = 'list_of_ids' elif isinstance(model_field, BooleanField): field_info['type'] = 'boolean' return field_info def get_serializer_info(self, serializer, method=None): filterer = getattr(serializer, 'filter_field_metadata', lambda fields, method: fields) return filterer( super(Metadata, self).get_serializer_info(serializer), method ) def determine_actions(self, request, view): # Add field information for GET requests (so field names/labels are # available even when we can't POST/PUT). actions = {} for method in {'GET', 'PUT', 'POST'} & set(view.allowed_methods): view.request = clone_request(request, method) obj = None try: # Test global permissions if hasattr(view, 'check_permissions'): view.check_permissions(view.request) # Test object permissions if method == 'PUT' and hasattr(view, 'get_object'): obj = view.get_object() except (exceptions.APIException, PermissionDenied, Http404): continue else: # If user has appropriate permissions for the view, include # appropriate metadata about the fields that should be supplied. serializer = view.get_serializer(instance=obj) actions[method] = self.get_serializer_info(serializer, method=method) finally: view.request = request for field, meta in list(actions[method].items()): if not isinstance(meta, dict): continue if field == "pod_spec_override": meta['default'] = PodManager().pod_definition # Add type choices if available from the serializer. if field == 'type' and hasattr(serializer, 'get_type_choices'): meta['choices'] = serializer.get_type_choices() # For GET method, remove meta attributes that aren't relevant # when reading a field and remove write-only fields. if method == 'GET': attrs_to_remove = ('required', 'read_only', 'default', 'min_length', 'max_length', 'placeholder') for attr in attrs_to_remove: meta.pop(attr, None) meta.get('child', {}).pop(attr, None) if meta.pop('write_only', False): actions['GET'].pop(field) # For PUT/POST methods, remove read-only fields. if method in ('PUT', 'POST'): # This value should always be False for PUT/POST, so don't # show it (file-based read-only settings can't be updated) meta.pop('defined_in_file', False) if meta.pop('read_only', False): if field == 'id' and hasattr(view, 'attach'): continue actions[method].pop(field) return actions def determine_metadata(self, request, view): # store request on self so we can use it to generate field defaults # (such as TOWER_URL_BASE) self.request = request try: setattr(view, '_request', request) metadata = super(Metadata, self).determine_metadata(request, view) finally: delattr(view, '_request') # Add type(s) handled by this view/serializer. if hasattr(view, 'get_serializer'): serializer = view.get_serializer() if hasattr(serializer, 'get_types'): metadata['types'] = serializer.get_types() # Add search fields if available from the view. if getattr(view, 'search_fields', None): metadata['search_fields'] = view.search_fields # Add related search fields if available from the view. if getattr(view, 'related_search_fields', None): metadata['related_search_fields'] = view.related_search_fields # include role names in metadata roles = [] model = getattr(view, 'model', None) if model: for field in model._meta.get_fields(): if type(field) is ImplicitRoleField: roles.append(field.name) if len(roles) > 0: metadata['object_roles'] = roles from rest_framework import generics if isinstance(view, generics.ListAPIView) and hasattr(view, 'paginator'): metadata['max_page_size'] = view.paginator.max_page_size return metadata class RoleMetadata(Metadata): def determine_metadata(self, request, view): metadata = super(RoleMetadata, self).determine_metadata(request, view) if 'actions' in metadata: metadata['actions'].pop('POST') metadata['actions']['POST'] = { "id": {"type": "integer", "label": "ID", "help_text": "Database ID for this role."}, "disassociate": {"type": "integer", "label": "Disassociate", "help_text": "Provide to remove this role."}, } return metadata class SublistAttachDetatchMetadata(Metadata): def determine_actions(self, request, view): actions = super(SublistAttachDetatchMetadata, self).determine_actions(request, view) method = 'POST' if method in actions: for field in list(actions[method].keys()): if field == 'id': continue actions[method].pop(field) return actions