1
# Python
2 3
import copy
3 3
import logging
4 3
from collections import OrderedDict
5

6
# Django
7 3
from django.core.exceptions import ObjectDoesNotExist, ValidationError as DjangoValidationError
8 3
from django.db import models
9 3
from django.utils.translation import ugettext_lazy as _
10 3
from django.utils.encoding import force_text
11 3
from django.utils.text import capfirst
12 3
from django.utils.timezone import now
13

14
# Django REST Framework
15 3
from rest_framework.exceptions import ValidationError
16 3
from rest_framework import fields
17 3
from rest_framework import serializers
18 3
from rest_framework import validators
19

20
# Django-Polymorphic
21 3
from polymorphic.models import PolymorphicModel
22

23
# cyborgbackup
24 3
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
25

26 3
from cyborgbackup.main.constants import ANSI_SGR_PATTERN
27 3
from cyborgbackup.main.models.events import JobEvent
28 3
from cyborgbackup.main.models.clients import Client
29 3
from cyborgbackup.main.models.catalogs import Catalog
30 3
from cyborgbackup.main.models.schedules import Schedule
31 3
from cyborgbackup.main.models.repositories import Repository
32 3
from cyborgbackup.main.models.policies import Policy
33 3
from cyborgbackup.main.models.jobs import Job
34 3
from cyborgbackup.main.models.users import User
35 3
from cyborgbackup.main.models.settings import Setting
36 3
from cyborgbackup.main.constants import ACTIVE_STATES
37 3
from cyborgbackup.main.utils.common import (
38
    get_type_for_model, get_model_for_type, camelcase_to_underscore,
39
    has_model_field_prefetched, prefetch_page_capabilities)
40 3
from cyborgbackup.main.validators import vars_validate_or_raise
41 3
from cyborgbackup.api.versioning import reverse, get_request_version
42 3
from cyborgbackup.api.fields import BooleanNullField, CharNullField, ChoiceNullField, VerbatimField
43

44 3
logger = logging.getLogger('cyborgbackup.api.serializers')
45

46 3
DEPRECATED = 'This resource has been deprecated and will be removed in a future release'
47

48

49
# Fields that should be summarized regardless of object type.
50 3
DEFAULT_SUMMARY_FIELDS = ('id', 'name', 'created_by', 'modified_by')
51

52

53
# Keys are fields (foreign keys) where, if found on an instance, summary info
54
# should be added to the serialized data.  Values are a tuple of field names on
55
# the related object to include in the summary data (if the field is present on
56
# the related object).
57 3
SUMMARIZABLE_FK_FIELDS = {
58
    'user': ('id', 'email', 'first_name', 'last_name'),
59
    'application': ('id', 'name', 'client_id'),
60
    'job': ('id', 'name', 'status', 'failed', 'elapsed'),
61
    'policy': ('id', 'name', 'policy_type'),
62
    'client': ('id', 'hostname', 'bandwidth_limit'),
63
    'repository': ('id', 'name', 'path', 'enabled'),
64
    'schedule': ('id', 'name', 'crontab', 'enabled')
65
}
66

67

68 3
def reverse_gfk(content_object, request):
69
    '''
70
    Computes a reverse for a GenericForeignKey field.
71

72
    Returns a dictionary of the form
73
        { '<type>': reverse(<type detail>) }
74
    for example
75
        { 'organization': '/api/v1/organizations/1/' }
76
    '''
77 0
    if content_object is None or not hasattr(content_object, 'get_absolute_url'):
78 0
        return {}
79

80 0
    return {
81
        camelcase_to_underscore(content_object.__class__.__name__): content_object.get_absolute_url(request=request)
82
    }
83

84

85 3
class DynamicFieldsSerializerMixin(object):
86
    """
87
    A serializer mixin that takes an additional `fields` argument that controls
88
    which fields should be displayed.
89
    """
90

91 3
    def __init__(self, *args, **kwargs):
92
        # Don't pass the 'fields' arg up to the superclass
93 3
        fields = kwargs.pop('fields', None)
94

95
        # Instantiate the superclass normally
96 3
        super(DynamicFieldsSerializerMixin, self).__init__(*args, **kwargs)
97

98 3
        if fields is not None:
99
            # Drop any fields that are not specified in the `fields` argument.
100 0
            allowed = set(fields)
101 0
            existing = set(self.fields.keys())
102 0
            for field_name in existing - allowed:
103 0
                self.fields.pop(field_name)
104

105

106 3
class BaseSerializerMetaclass(serializers.SerializerMetaclass):
107
    '''
108
    Custom metaclass to enable attribute inheritance from Meta objects on
109
    serializer base classes.
110

111
    Also allows for inheriting or updating field lists from base class(es):
112

113
        class Meta:
114

115
            # Inherit all fields from base class.
116
            fields = ('*',)
117

118
            # Inherit all fields from base class and add 'foo'.
119
            fields = ('*', 'foo')
120

121
            # Inherit all fields from base class except 'bar'.
122
            fields = ('*', '-bar')
123

124
            # Define fields as 'foo' and 'bar'; ignore base class fields.
125
            fields = ('foo', 'bar')
126

127
            # Extra field kwargs dicts are also merged from base classes.
128
            extra_kwargs = {
129
                'foo': {'required': True},
130
                'bar': {'read_only': True},
131
            }
132

133
            # If a subclass were to define extra_kwargs as:
134
            extra_kwargs = {
135
                'foo': {'required': False, 'default': ''},
136
                'bar': {'label': 'New Label for Bar'},
137
            }
138

139
            # The resulting value of extra_kwargs would be:
140
            extra_kwargs = {
141
                'foo': {'required': False, 'default': ''},
142
                'bar': {'read_only': True, 'label': 'New Label for Bar'},
143
            }
144

145
            # Extra field kwargs cannot be removed in subclasses, only replaced.
146

147
    '''
148

149 3
    @staticmethod
150
    def _is_list_of_strings(x):
151 3
        return isinstance(x, (list, tuple)) and all([isinstance(y, str) for y in x])
152

153 3
    @staticmethod
154
    def _is_extra_kwargs(x):
155 3
        return isinstance(x, dict) and all([isinstance(k, str) and isinstance(v, dict) for k, v in x.items()])
156

157 3
    @classmethod
158 3
    def _update_meta(cls, base, meta, other=None):
159 3
        for attr in dir(other):
160 3
            if attr.startswith('_'):
161 3
                continue
162 3
            val = getattr(other, attr)
163 3
            meta_val = getattr(meta, attr, None)
164
            # Special handling for lists/tuples of strings (field names).
165 3
            if cls._is_list_of_strings(val) and cls._is_list_of_strings(meta_val or []):
166 3
                meta_val = meta_val or []
167 3
                new_vals = []
168 3
                except_vals = []
169 3
                if base:
170 3
                    new_vals.extend([x for x in meta_val])
171 3
                for v in val:
172 3
                    if not base and v == '*':
173 3
                        new_vals.extend([x for x in meta_val])
174 3
                    elif not base and v.startswith('-'):
175 3
                        except_vals.append(v[1:])
176
                    else:
177 3
                        new_vals.append(v)
178 3
                val = []
179 3
                for v in new_vals:
180 3
                    if v not in except_vals and v not in val:
181 3
                        val.append(v)
182 3
                val = tuple(val)
183
            # Merge extra_kwargs dicts from base classes.
184 3
            elif cls._is_extra_kwargs(val) and cls._is_extra_kwargs(meta_val or {}):
185 0
                meta_val = meta_val or {}
186 0
                new_val = {}
187 0
                if base:
188 0
                    for k, v in meta_val.items():
189 0
                        new_val[k] = copy.deepcopy(v)
190 0
                for k, v in val.items():
191 0
                    new_val.setdefault(k, {}).update(copy.deepcopy(v))
192 0
                val = new_val
193
            # Any other values are copied in case they are mutable objects.
194
            else:
195 3
                val = copy.deepcopy(val)
196 3
            setattr(meta, attr, val)
197

198 3
    def __new__(cls, name, bases, attrs):
199 3
        meta = type('Meta', (object,), {})
200 3
        for base in bases[::-1]:
201 3
            cls._update_meta(base, meta, getattr(base, 'Meta', None))
202 3
        cls._update_meta(None, meta, attrs.get('Meta', meta))
203 3
        attrs['Meta'] = meta
204 3
        return super(BaseSerializerMetaclass, cls).__new__(cls, name, bases, attrs)
205

206

207 3
class BaseSerializer(serializers.ModelSerializer, metaclass=BaseSerializerMetaclass):
208

209 3
    class Meta:
210 3
        ordering = ('id',)
211 3
        fields = ('id', 'type', 'url', 'related', 'summary_fields', 'created',
212
                  'modified', 'name', 'created_by', 'modified_by')
213 3
        summary_fields = ()
214 3
        summarizable_fields = ()
215

216
    # add the URL and related resources
217 3
    type = serializers.SerializerMethodField()
218 3
    url = serializers.SerializerMethodField()
219 3
    related = serializers.SerializerMethodField('_get_related')
220 3
    summary_fields = serializers.SerializerMethodField('_get_summary_fields')
221

222
    # make certain fields read only
223 3
    created = serializers.SerializerMethodField()
224 3
    modified = serializers.SerializerMethodField()
225

226 3
    @property
227
    def version(self):
228
        """
229
        The request version component of the URL as an integer i.e., 1 or 2
230
        """
231 3
        return get_request_version(self.context.get('request'))
232

233 3
    def get_type(self, obj):
234 3
        return get_type_for_model(self.Meta.model)
235

236 3
    def get_types(self):
237 0
        return [self.get_type(None)]
238

239 3
    def get_type_choices(self):
240 0
        type_name_map = {
241
            'job': 'Job',
242
            'image': 'Image',
243
            'client': 'Client',
244
            'schedule': 'Schedule',
245
            'repository': 'Repository',
246
            'user': 'User'
247
        }
248 0
        choices = []
249 0
        for t in self.get_types():
250 0
            name = type_name_map.get(t, force_text(get_model_for_type(t)._meta.verbose_name).title())
251 0
            choices.append((t, name))
252 0
        return choices
253

254 3
    def get_url(self, obj):
255 3
        if isinstance(obj, User):
256 3
            return self.reverse('api:user_detail', kwargs={'pk': obj.pk})
257 3
        elif obj is None or not hasattr(obj, 'get_absolute_url'):
258 0
            return ''
259
        else:
260 3
            return obj.get_absolute_url(request=self.context.get('request'))
261

262 3
    def filter_field_metadata(self, fields, method):
263
        """
264
        Filter field metadata based on the request method.
265
        This it intended to be extended by subclasses.
266
        """
267 0
        return fields
268

269 3
    def _get_related(self, obj):
270 3
        return {} if obj is None else self.get_related(obj)
271

272 3
    def _generate_named_url(self, url_path, obj, node):
273 0
        url_units = url_path.split('/')
274 0
        named_url = node.generate_named_url(obj)
275 0
        url_units[4] = named_url
276 0
        return '/'.join(url_units)
277

278 3
    def get_related(self, obj):
279 3
        res = OrderedDict()
280 3
        if getattr(obj, 'created_by', None):
281 3
            res['created_by'] = self.reverse('api:user_detail', kwargs={'pk': obj.created_by.pk})
282 3
        if getattr(obj, 'modified_by', None):
283 3
            res['modified_by'] = self.reverse('api:user_detail', kwargs={'pk': obj.modified_by.pk})
284 3
        return res
285

286 3
    def _get_summary_fields(self, obj):
287 3
        return {} if obj is None else self.get_summary_fields(obj)
288

289 3
    def get_summary_fields(self, obj):
290
        # Return values for certain fields on related objects, to simplify
291
        # displaying lists of items without additional API requests.
292 3
        summary_fields = OrderedDict()
293 3
        for fk, related_fields in SUMMARIZABLE_FK_FIELDS.items():
294 3
            try:
295 3
                fkval = getattr(obj, fk, None)
296 3
                if fkval is None:
297 3
                    continue
298 3
                if fkval == obj:
299 0
                    continue
300 3
                summary_fields[fk] = OrderedDict()
301 3
                for field in related_fields:
302 3
                    if (
303
                            self.version < 2 and field == 'credential_type_id' and
304
                            fk in ['credential', 'vault_credential']):
305 0
                        continue
306

307 3
                    fval = getattr(fkval, field, None)
308

309 3
                    if fval is None and field == 'type':
310 0
                        if isinstance(fkval, PolymorphicModel):
311 0
                            fkval = fkval.get_real_instance()
312 0
                        fval = get_type_for_model(fkval)
313 3
                    if fval is not None:
314 3
                        summary_fields[fk][field] = fval
315
            # Can be raised by the reverse accessor for a OneToOneField.
316 0
            except ObjectDoesNotExist:
317 0
                pass
318 3
        if getattr(obj, 'created_by', None):
319 3
            summary_fields['created_by'] = OrderedDict()
320 3
            for field in SUMMARIZABLE_FK_FIELDS['user']:
321 3
                summary_fields['created_by'][field] = getattr(obj.created_by, field)
322 3
        if getattr(obj, 'modified_by', None):
323 3
            summary_fields['modified_by'] = OrderedDict()
324 3
            for field in SUMMARIZABLE_FK_FIELDS['user']:
325 3
                summary_fields['modified_by'][field] = getattr(obj.modified_by, field)
326

327 3
        return summary_fields
328

329 3
    def _obj_capability_dict(self, obj):
330
        """
331
        Returns the user_capabilities dictionary for a single item
332
        If inside of a list view, it runs the prefetching algorithm for
333
        the entire current page, saves it into context
334
        """
335 0
        view = self.context.get('view', None)
336 0
        parent_obj = None
337 0
        if view and hasattr(view, 'parent_model') and hasattr(view, 'get_parent_object'):
338 0
            parent_obj = view.get_parent_object()
339 0
        if view and view.request and view.request.user:
340 0
            capabilities_cache = {}
341
            # if serializer has parent, it is ListView, apply page capabilities prefetch
342 0
            if self.parent and hasattr(self, 'capabilities_prefetch') and self.capabilities_prefetch:
343 0
                qs = self.parent.instance
344 0
                if 'capability_map' not in self.context:
345 0
                    model = self.Meta.model
346 0
                    prefetch_list = self.capabilities_prefetch
347 0
                    self.context['capability_map'] = prefetch_page_capabilities(
348
                        model, qs, prefetch_list, view.request.user
349
                    )
350 0
                if obj.id in self.context['capability_map']:
351 0
                    capabilities_cache = self.context['capability_map'][obj.id]
352 0
            return {parent_obj, capabilities_cache}
353
            # return get_user_capabilities(
354
            #     view.request.user, obj, method_list=self.show_capabilities, parent_obj=parent_obj,
355
            #     capabilities_cache=capabilities_cache
356
            # )
357
        else:
358
            # Contextual information to produce user_capabilities doesn't exist
359 0
            return {}
360

361 3
    def get_created(self, obj):
362 3
        if obj is None:
363 0
            return None
364 3
        elif isinstance(obj, User):
365 3
            return obj.date_joined
366 3
        elif hasattr(obj, 'created'):
367 3
            return obj.created
368 0
        return None
369

370 3
    def get_modified(self, obj):
371 3
        if obj is None:
372 0
            return None
373 3
        elif isinstance(obj, User):
374 0
            return obj.last_login
375 3
        elif hasattr(obj, 'modified'):
376 3
            return obj.modified
377 0
        return None
378

379 3
    def get_extra_kwargs(self):
380 3
        extra_kwargs = super(BaseSerializer, self).get_extra_kwargs()
381 3
        if self.instance:
382 3
            read_only_on_update_fields = getattr(self.Meta, 'read_only_on_update_fields', tuple())
383 3
            for field_name in read_only_on_update_fields:
384 0
                kwargs = extra_kwargs.get(field_name, {})
385 0
                kwargs['read_only'] = True
386 0
                extra_kwargs[field_name] = kwargs
387 3
        return extra_kwargs
388

389 3
    def build_standard_field(self, field_name, model_field):
390
        # DRF 3.3 serializers.py::build_standard_field() -> utils/field_mapping.py::get_field_kwargs() short circuits
391
        # when a Model's editable field is set to False. The short circuit skips choice rendering.
392
        #
393
        # This logic is to force rendering choice's on an uneditable field.
394
        # Note: Consider expanding this rendering for more than just choices fields
395
        # Note: This logic works in conjuction with
396 3
        if hasattr(model_field, 'choices') and model_field.choices:
397 3
            was_editable = model_field.editable
398 3
            model_field.editable = True
399

400 3
        field_class, field_kwargs = super(BaseSerializer, self).build_standard_field(field_name, model_field)
401 3
        if hasattr(model_field, 'choices') and model_field.choices:
402 3
            model_field.editable = was_editable
403 3
            if was_editable is False:
404 3
                field_kwargs['read_only'] = True
405

406
        # Pass model field default onto the serializer field if field is not read-only.
407 3
        if model_field.has_default() and not field_kwargs.get('read_only', False):
408 3
            field_kwargs['default'] = field_kwargs['initial'] = model_field.get_default()
409

410
        # Enforce minimum value of 0 for PositiveIntegerFields.
411 3
        if isinstance(model_field, (models.PositiveIntegerField, models.PositiveSmallIntegerField)) \
412
                and 'choices' not in field_kwargs:
413 3
            field_kwargs['min_value'] = 0
414

415
        # Use custom boolean field that allows null and empty string as False values.
416 3
        if isinstance(model_field, models.BooleanField) and not field_kwargs.get('read_only', False):
417 3
            field_class = BooleanNullField
418

419
        # Use custom char or choice field that coerces null to an empty string.
420 3
        if isinstance(model_field, (models.CharField, models.TextField)) and not field_kwargs.get('read_only', False):
421 3
            if 'choices' in field_kwargs:
422 3
                field_class = ChoiceNullField
423
            else:
424 3
                field_class = CharNullField
425

426
        # Update the message used for the unique validator to use capitalized
427
        # verbose name; keeps unique message the same as with DRF 2.x.
428 3
        opts = self.Meta.model._meta.concrete_model._meta
429 3
        for validator in field_kwargs.get('validators', []):
430 3
            if isinstance(validator, validators.UniqueValidator):
431 3
                unique_error_message = model_field.error_messages.get('unique', None)
432 3
                if unique_error_message:
433 3
                    unique_error_message = unique_error_message % {
434
                        'model_name': capfirst(opts.verbose_name),
435
                        'field_label': capfirst(model_field.verbose_name),
436
                    }
437 3
                    validator.message = unique_error_message
438

439 3
        return field_class, field_kwargs
440

441 3
    def build_relational_field(self, field_name, relation_info):
442 3
        field_class, field_kwargs = super(BaseSerializer, self).build_relational_field(field_name, relation_info)
443
        # Don't include choices for foreign key fields.
444 3
        field_kwargs.pop('choices', None)
445 3
        return field_class, field_kwargs
446

447 3
    def get_unique_together_validators(self):
448
        # Allow the model's full_clean method to handle the unique together validation.
449 3
        return []
450

451 3
    def run_validation(self, data=fields.empty):
452 3
        try:
453 3
            return super(BaseSerializer, self).run_validation(data)
454 0
        except ValidationError as exc:
455
            # Avoid bug? in DRF if exc.detail happens to be a list instead of a dict.
456 0
            raise ValidationError(detail=serializers.as_serializer_error(exc))
457

458 3
    def get_validation_exclusions(self, obj=None):
459
        # Borrowed from DRF 2.x - return model fields that should be excluded
460
        # from model validation.
461 3
        cls = self.Meta.model
462 3
        opts = cls._meta.concrete_model._meta
463 3
        exclusions = [field.name for field in opts.fields]
464 3
        for field_name, field in self.fields.items():
465 3
            field_name = field.source or field_name
466 3
            if field_name not in exclusions:
467 3
                continue
468 3
            if field.read_only:
469 3
                continue
470 3
            if isinstance(field, serializers.Serializer):
471 0
                continue
472 3
            exclusions.remove(field_name)
473
        # The clean_ methods cannot be ran on many-to-many models
474 3
        exclusions.extend([field.name for field in opts.many_to_many])
475 3
        return exclusions
476

477 3
    def validate(self, attrs):
478 3
        attrs = super(BaseSerializer, self).validate(attrs)
479 3
        try:
480
            # Create/update a model instance and run it's full_clean() method to
481
            # do any validation implemented on the model class.
482 3
            exclusions = self.get_validation_exclusions(self.instance)
483 3
            obj = self.instance or self.Meta.model()
484 3
            for k, v in attrs.items():
485 3
                if k not in exclusions:
486 3
                    setattr(obj, k, v)
487 3
            obj.full_clean(exclude=exclusions)
488
            # full_clean may modify values on the instance; copy those changes
489
            # back to attrs so they are saved.
490 3
            for k in attrs.keys():
491 3
                if k not in exclusions:
492 3
                    attrs[k] = getattr(obj, k)
493 0
        except DjangoValidationError as exc:
494
            # DjangoValidationError may contain a list or dict; normalize into a
495
            # dict where the keys are the field name and the values are a list
496
            # of error messages, then raise as a DRF ValidationError.  DRF would
497
            # normally convert any DjangoValidationError to a non-field specific
498
            # error message; here we preserve field-specific errors raised from
499
            # the model's full_clean method.
500 0
            d = exc.update_error_dict({})
501 0
            for k, v in d.items():
502 0
                v = v if isinstance(v, list) else [v]
503 0
                v2 = []
504 0
                for e in v:
505 0
                    if isinstance(e, DjangoValidationError):
506 0
                        v2.extend(list(e))
507 0
                    elif isinstance(e, list):
508 0
                        v2.extend(e)
509
                    else:
510 0
                        v2.append(e)
511 0
                d[k] = map(force_text, v2)
512 0
            raise ValidationError(d)
513 3
        return attrs
514

515 3
    def reverse(self, *args, **kwargs):
516 3
        kwargs['request'] = self.context.get('request')
517 3
        return reverse(*args, **kwargs)
518

519 3
    @property
520
    def is_detail_view(self):
521 0
        if 'view' in self.context:
522 0
            if 'pk' in self.context['view'].kwargs:
523 0
                return True
524 0
        return False
525

526

527 3
class EmptySerializer(serializers.Serializer):
528 3
    pass
529

530

531 3
class UserSerializer(BaseSerializer):
532

533 3
    password = serializers.CharField(required=False, default='', write_only=True,
534
                                     help_text=_('Write-only field used to change the password.'))
535 3
    show_capabilities = ['edit', 'delete']
536

537 3
    class Meta:
538 3
        model = User
539 3
        fields = ('*', '-name', '-description', '-modified', '-username',
540
                  'first_name', 'last_name', 'email', 'is_superuser', 'password',
541
                  '-created_by', '-modified_by', 'notify_backup_daily',
542
                  'notify_backup_weekly', 'notify_backup_monthly',
543
                  'notify_backup_success', 'notify_backup_failed',
544
                  'notify_backup_summary')
545

546 3
    def to_representation(self, obj):
547 3
        ret = super(UserSerializer, self).to_representation(obj)
548 3
        ret.pop('password', None)
549 3
        return ret
550

551 3
    def get_validation_exclusions(self, obj=None):
552 0
        ret = super(UserSerializer, self).get_validation_exclusions(obj)
553 0
        ret.append('password')
554 0
        return ret
555

556 3
    def validate_password(self, value):
557 0
        if not self.instance and value in (None, ''):
558 0
            raise serializers.ValidationError(_('Password required for new User.'))
559 0
        return value
560

561 3
    def _update_password(self, obj, new_password):
562
        # For now we're not raising an error, just not saving password for
563
        # users managed by LDAP who already have an unusable password set.
564 0
        if new_password:
565 0
            obj.set_password(new_password)
566 0
            obj.save(update_fields=['password'])
567 0
        elif not obj.password:
568 0
            obj.set_unusable_password()
569 0
            obj.save(update_fields=['password'])
570

571 3
    def create(self, validated_data):
572 0
        new_password = validated_data.pop('password', None)
573 0
        obj = super(UserSerializer, self).create(validated_data)
574 0
        self._update_password(obj, new_password)
575 0
        return obj
576

577 3
    def update(self, obj, validated_data):
578 0
        new_password = validated_data.pop('password', None)
579 0
        obj = super(UserSerializer, self).update(obj, validated_data)
580 0
        self._update_password(obj, new_password)
581 0
        return obj
582

583

584 3
class BaseSerializerWithVariables(BaseSerializer):
585

586 3
    def validate_variables(self, value):
587 0
        return vars_validate_or_raise(value)
588

589

590 3
class LabelsListMixin(object):
591

592 3
    def _summary_field_labels(self, obj):
593 0
        label_list = [{'id': x.id, 'name': x.name} for x in obj.labels.all()[:10]]
594 0
        if has_model_field_prefetched(obj, 'labels'):
595 0
            label_ct = len(obj.labels.all())
596
        else:
597 0
            if len(label_list) < 10:
598 0
                label_ct = len(label_list)
599
            else:
600 0
                label_ct = obj.labels.count()
601 0
        return {'count': label_ct, 'results': label_list}
602

603 3
    def get_summary_fields(self, obj):
604 0
        res = super(LabelsListMixin, self).get_summary_fields(obj)
605 0
        res['labels'] = self._summary_field_labels(obj)
606 0
        return res
607

608

609 3
class JobSerializer(BaseSerializer):
610

611 3
    show_capabilities = ['start', 'delete']
612 3
    event_processing_finished = serializers.BooleanField(
613
        help_text=_('Indicates whether all of the events generated by this '
614
                    'unified job have been saved to the database.'),
615
        read_only=True
616
    )
617

618 3
    class Meta:
619 3
        model = Job
620 3
        fields = ('*', 'launch_type', 'status', 'policy',
621
                  'failed', 'started', 'finished', 'elapsed', 'job_args',
622
                  'original_size', 'compressed_size', 'deduplicated_size', 'archive_name',
623
                  'job_cwd', 'job_env', 'job_explanation', 'client', 'repository',
624
                  'dependent_jobs', 'result_traceback', 'event_processing_finished', 'job_type')
625

626 3
    def get_types(self):
627 0
        if type(self) is JobSerializer:
628 0
            return ['job', ]
629
        else:
630 0
            return super(JobSerializer, self).get_types()
631

632 3
    def get_summary_fields(self, obj):
633 0
        summary_dict = super(JobSerializer, self).get_summary_fields(obj)
634 0
        if obj.policy and obj.policy.repository_id:
635 0
            summary_dict['repository'] = {
636
                'id': obj.policy.repository_id,
637
                'name': obj.policy.repository.name
638
            }
639

640 0
        if obj.policy and obj.policy.schedule_id:
641 0
            summary_dict['schedule'] = {
642
                'id': obj.policy.schedule_id,
643
                'name': obj.policy.schedule.name
644
            }
645

646 0
        return summary_dict
647

648 3
    def get_related(self, obj):
649 0
        res = super(JobSerializer, self).get_related(obj)
650 0
        res.update(dict(
651
            job_events=self.reverse('api:job_job_events_list', kwargs={'pk': obj.pk}),
652
        ))
653 0
        if obj.policy_id:
654 0
            res['policy'] = self.reverse('api:policy_detail', kwargs={'pk': obj.policy_id})
655 0
        res['stdout'] = self.reverse('api:job_stdout', kwargs={'pk': obj.pk})
656 0
        if (obj.can_start or True):
657 0
            res['start'] = self.reverse('api:job_start', kwargs={'pk': obj.pk})
658 0
        if obj.can_cancel or True:
659 0
            res['cancel'] = self.reverse('api:job_cancel', kwargs={'pk': obj.pk})
660 0
        res['relaunch'] = self.reverse('api:job_relaunch', kwargs={'pk': obj.pk})
661 0
        return res
662

663 3
    def get_artifacts(self, obj):
664 0
        if obj:
665 0
            return obj.display_artifacts()
666 0
        return {}
667

668 3
    def to_internal_value(self, data):
669 0
        return super(JobSerializer, self).to_internal_value(data)
670

671 3
    def to_representation(self, obj):
672 0
        ret = super(JobSerializer, self).to_representation(obj)
673 0
        serializer_class = None
674 0
        if serializer_class:
675 0
            serializer = serializer_class(instance=obj, context=self.context)
676 0
            ret = serializer.to_representation(obj)
677
        else:
678 0
            ret = super(JobSerializer, self).to_representation(obj)
679

680 0
        if 'elapsed' in ret:
681 0
            if obj and obj.pk and obj.started and not obj.finished:
682 0
                td = now() - obj.started
683 0
                ret['elapsed'] = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / (10 ** 6 * 1.0)
684 0
            ret['elapsed'] = float(ret['elapsed'])
685

686 0
        return ret
687

688

689 3
class JobStdoutSerializer(JobSerializer):
690

691 3
    result_stdout = serializers.SerializerMethodField()
692

693 3
    class Meta:
694 3
        fields = ('result_stdout',)
695

696 3
    def get_types(self):
697 0
        if type(self) is JobStdoutSerializer:
698 0
            return ['job']
699
        else:
700 0
            return super(JobStdoutSerializer, self).get_types()
701

702

703 3
class JobCancelSerializer(JobSerializer):
704

705 3
    can_cancel = serializers.BooleanField(read_only=True)
706

707 3
    class Meta:
708 3
        fields = ('can_cancel',)
709

710

711 3
class JobRelaunchSerializer(BaseSerializer):
712

713 3
    retry_counts = serializers.SerializerMethodField()
714

715 3
    class Meta:
716 3
        model = Job
717 3
        fields = ('retry_counts',)
718

719 3
    def get_retry_counts(self, obj):
720 0
        if obj.status in ACTIVE_STATES:
721 0
            return _('Relaunch by host status not available until job finishes running.')
722 0
        data = OrderedDict([])
723 0
        return data
724

725 3
    def get_validation_exclusions(self, *args, **kwargs):
726 0
        r = super(JobRelaunchSerializer, self).get_validation_exclusions(*args, **kwargs)
727 0
        return r
728

729 3
    def validate(self, attrs):
730 0
        attrs = super(JobRelaunchSerializer, self).validate(attrs)
731 0
        return attrs
732

733

734 3
class JobListSerializer(DynamicFieldsSerializerMixin, JobSerializer):
735

736 3
    class Meta:
737 3
        fields = ('*', '-job_args', '-job_cwd', '-job_env', '-result_traceback', '-event_processing_finished')
738

739 3
    def get_field_names(self, declared_fields, info):
740 0
        field_names = super(JobListSerializer, self).get_field_names(declared_fields, info)
741
        # Meta multiple inheritance and -field_name options don't seem to be
742
        # taking effect above, so remove the undesired fields here.
743 0
        return tuple(x for x in field_names if x not in (
744
                                                         'job_args', 'job_cwd',
745
                                                         'job_env', 'result_traceback', 'event_processing_finished'
746
        ))
747

748 3
    def get_types(self):
749 0
        if type(self) is JobListSerializer:
750 0
            return ['job', ]
751
        else:
752 0
            return super(JobListSerializer, self).get_types()
753

754 3
    def to_representation(self, obj):
755 0
        serializer_class = None
756 0
        if type(self) is JobListSerializer:
757 0
            if isinstance(obj, Job):
758 0
                serializer_class = JobSerializer
759 0
        if serializer_class:
760 0
            serializer = serializer_class(instance=obj, context=self.context)
761 0
            ret = serializer.to_representation(obj)
762
        else:
763 0
            ret = super(JobListSerializer, self).to_representation(obj)
764 0
        if 'elapsed' in ret:
765 0
            ret['elapsed'] = float(ret['elapsed'])
766 0
        return ret
767

768

769 3
class JobEventSerializer(BaseSerializer):
770

771 3
    event_display = serializers.CharField(source='get_event_display2', read_only=True)
772 3
    event_level = serializers.IntegerField(read_only=True)
773

774 3
    class Meta:
775 3
        model = JobEvent
776 3
        fields = ('*', '-name', '-description', 'job', 'event', 'counter',
777
                  'event_display', 'event_data', 'event_level', 'failed',
778
                  'changed', 'uuid', 'task', 'stdout', 'start_line', 'end_line',
779
                  'verbosity', '-created_by', '-modified_by')
780

781 3
    def get_related(self, obj):
782 0
        res = super(JobEventSerializer, self).get_related(obj)
783 0
        res.update(dict(
784
            job=self.reverse('api:job_detail', kwargs={'pk': obj.job_id}),
785
        ))
786 0
        return res
787

788 3
    def get_summary_fields(self, obj):
789 0
        d = super(JobEventSerializer, self).get_summary_fields(obj)
790 0
        return d
791

792 3
    def to_representation(self, obj):
793 0
        ret = super(JobEventSerializer, self).to_representation(obj)
794
        # Show full stdout for event detail view, truncate only for list view.
795 0
        if hasattr(self.context.get('view', None), 'retrieve'):
796 0
            return ret
797
        # Show full stdout for playbook_on_* events.
798 0
        if obj and obj.event.startswith('playbook_on'):
799 0
            return ret
800 0
        max_bytes = 1024
801 0
        if max_bytes > 0 and 'stdout' in ret and len(ret['stdout']) >= max_bytes:
802 0
            ret['stdout'] = ret['stdout'][:(max_bytes - 1)] + u'\u2026'
803 0
            set_count = 0
804 0
            reset_count = 0
805 0
            for m in ANSI_SGR_PATTERN.finditer(ret['stdout']):
806 0
                if m.string[m.start():m.end()] == u'\u001b[0m':
807 0
                    reset_count += 1
808
                else:
809 0
                    set_count += 1
810 0
            ret['stdout'] += u'\u001b[0m' * (set_count - reset_count)
811 0
        return ret
812

813

814 3
class JobEventWebSocketSerializer(JobEventSerializer):
815 3
    created = serializers.SerializerMethodField()
816 3
    modified = serializers.SerializerMethodField()
817 3
    event_name = serializers.CharField(source='event')
818 3
    group_name = serializers.SerializerMethodField()
819

820 3
    class Meta:
821 3
        model = JobEvent
822 3
        fields = ('*', 'event_name', 'group_name',)
823

824 3
    def get_created(self, obj):
825 0
        return obj.created.isoformat()
826

827 3
    def get_modified(self, obj):
828 0
        return obj.modified.isoformat()
829

830 3
    def get_group_name(self, obj):
831 0
        return 'job_events'
832

833

834 3
class SettingSerializer(BaseSerializer):
835
    """Read-only serializer for activity stream."""
836

837 3
    value = VerbatimField(allow_null=True)
838

839 3
    class Meta:
840 3
        model = Setting
841 3
        fields = ('id', 'url', 'key', 'type', 'setting_type', 'value', 'created', 'modified')
842

843 3
    def update(self, obj, validated_data):
844 0
        validated_data.pop('key', None)
845 0
        obj = super(SettingSerializer, self).update(obj, validated_data)
846 0
        return obj
847

848 3
    def validate(self, attrs):
849 0
        attrs.pop('key', None)
850 0
        return attrs
851

852

853 3
class SettingListSerializer(SettingSerializer):
854

855 3
    class Meta:
856 3
        fields = ('*',)
857

858 3
    def get_field_names(self, declared_fields, info):
859 3
        field_names = super(SettingListSerializer, self).get_field_names(declared_fields, info)
860
        # Meta multiple inheritance and -field_name options don't seem to be
861
        # taking effect above, so remove the undesired fields here.
862 3
        return tuple(x for x in field_names)
863

864 3
    def get_types(self):
865 0
        if type(self) is SettingListSerializer:
866 0
            return ['setting']
867
        else:
868 0
            return super(SettingListSerializer, self).get_types()
869

870

871 3
class ClientSerializer(BaseSerializer):
872 3
    show_capabilities = ['edit', 'delete']
873

874 3
    class Meta:
875 3
        model = Client
876 3
        fields = ('*', '-name', '-description', 'hostname', 'ip', 'bandwidth_limit',
877
                  'version', 'ready', 'hypervisor_ready', 'hypervisor_name', 'enabled', 'uuid')
878

879 3
    def get_summary_fields(self, obj):
880 3
        summary_dict = super(ClientSerializer, self).get_summary_fields(obj)
881 3
        relPolicies = Policy.objects.filter(clients__id=obj.pk)
882 3
        if relPolicies.exists():
883 0
            summary_dict['policies'] = []
884 0
            for pol in relPolicies:
885 0
                summary_dict['policies'].append({'id': pol.id, 'name': pol.name})
886

887 3
        return summary_dict
888

889

890 3
class ClientListSerializer(ClientSerializer):
891

892 3
    class Meta:
893 3
        fields = ('*',)
894

895 3
    def get_field_names(self, declared_fields, info):
896 3
        field_names = super(ClientListSerializer, self).get_field_names(declared_fields, info)
897 3
        return tuple(x for x in field_names)
898

899 3
    def get_types(self):
900 0
        if type(self) is ClientListSerializer:
901 0
            return ['client']
902
        else:
903 0
            return super(ClientListSerializer, self).get_types()
904

905

906 3
class ScheduleSerializer(BaseSerializer):
907
    """Read-only serializer for activity stream."""
908

909 3
    class Meta:
910 3
        model = Schedule
911 3
        fields = ('id', 'uuid', 'url', 'name', 'crontab', 'enabled', 'created', 'modified')
912

913 3
    def update(self, obj, validated_data):
914 3
        obj = super(ScheduleSerializer, self).update(obj, validated_data)
915 3
        return obj
916

917 3
    def validate(self, attrs):
918 3
        return attrs
919

920

921 3
class ScheduleListSerializer(ScheduleSerializer):
922

923 3
    class Meta:
924 3
        fields = ('*',)
925

926 3
    def get_field_names(self, declared_fields, info):
927 3
        field_names = super(ScheduleListSerializer, self).get_field_names(declared_fields, info)
928
        # Meta multiple inheritance and -field_name options don't seem to be
929
        # taking effect above, so remove the undesired fields here.
930 3
        return tuple(x for x in field_names)
931

932 3
    def get_types(self):
933 0
        if type(self) is ScheduleListSerializer:
934 0
            return ['schedule']
935
        else:
936 0
            return super(ScheduleListSerializer, self).get_types()
937

938

939 3
class RepositorySerializer(BaseSerializer):
940
    """Read-only serializer for activity stream."""
941

942 3
    class Meta:
943 3
        model = Repository
944 3
        fields = ('id', 'uuid', 'url', 'name', 'path', 'repository_key',
945
                  'original_size', 'compressed_size', 'deduplicated_size', 'ready', 'enabled', 'created', 'modified')
946

947 3
    def update(self, obj, validated_data):
948 3
        obj = super(RepositorySerializer, self).update(obj, validated_data)
949 3
        return obj
950

951 3
    def validate(self, attrs):
952 3
        return attrs
953

954

955 3
class RepositoryListSerializer(RepositorySerializer):
956

957 3
    class Meta:
958 3
        fields = ('*',)
959

960 3
    def get_field_names(self, declared_fields, info):
961 3
        field_names = super(RepositoryListSerializer, self).get_field_names(declared_fields, info)
962
        # Meta multiple inheritance and -field_name options don't seem to be
963
        # taking effect above, so remove the undesired fields here.
964 3
        return tuple(x for x in field_names)
965

966 3
    def get_types(self):
967 0
        if type(self) is RepositoryListSerializer:
968 0
            return ['repository']
969
        else:
970 0
            return super(RepositoryListSerializer, self).get_types()
971

972

973 3
class PolicySerializer(BaseSerializer):
974

975 3
    class Meta:
976 3
        model = Policy
977 3
        fields = ('*', 'id', 'uuid', 'url', 'name', 'extra_vars',
978
                  'clients', 'repository', 'schedule', 'policy_type', 'keep_hourly',
979
                  'keep_yearly', 'keep_daily', 'keep_weekly', 'keep_monthly',
980
                  'vmprovider', 'next_run', 'mode_pull', 'enabled', 'created', 'modified',
981
                  'prehook', 'posthook')
982

983 3
    def get_related(self, obj):
984 3
        res = super(PolicySerializer, self).get_related(obj)
985 3
        res['launch'] = self.reverse('api:policy_launch', kwargs={'pk': obj.pk})
986 3
        res['calendar'] = self.reverse('api:policy_calendar', kwargs={'pk': obj.pk})
987 3
        if obj.schedule:
988 3
            res['schedule'] = self.reverse('api:schedule_detail', kwargs={'pk': obj.schedule.pk})
989 3
        if obj.repository:
990 3
            res['repository'] = self.reverse('api:repository_detail', kwargs={'pk': obj.repository.pk})
991 3
        return res
992

993 3
    def to_representation(self, obj):
994 3
        ret = super(PolicySerializer, self).to_representation(obj)
995 3
        if obj is not None and 'schedule' in ret and not obj.schedule:
996 0
            ret['schedule'] = None
997 3
        if obj is not None and 'repository' in ret and not obj.repository:
998 0
            ret['repository'] = None
999 3
        return ret
1000

1001

1002 3
class PolicyListSerializer(PolicySerializer):
1003

1004 3
    class Meta:
1005 3
        fields = ('*',)
1006

1007 3
    def get_field_names(self, declared_fields, info):
1008 0
        field_names = super(PolicyListSerializer, self).get_field_names(declared_fields, info)
1009
        # Meta multiple inheritance and -field_name options don't seem to be
1010
        # taking effect above, so remove the undesired fields here.
1011 0
        return tuple(x for x in field_names)
1012

1013 3
    def get_types(self):
1014 0
        if type(self) is PolicyListSerializer:
1015 0
            return ['policy']
1016
        else:
1017 0
            return super(PolicyListSerializer, self).get_types()
1018

1019

1020 3
class PolicyLaunchSerializer(BaseSerializer):
1021 3
    defaults = serializers.SerializerMethodField()
1022 3
    extra_vars = serializers.JSONField(required=False, write_only=True)
1023 3
    verbosity = serializers.IntegerField(required=False, initial=0, min_value=0, max_value=4, write_only=True)
1024

1025 3
    class Meta:
1026 3
        model = Policy
1027 3
        fields = ('defaults', 'extra_vars', 'verbosity')
1028

1029 3
    def get_defaults(self, obj):
1030 0
        defaults_dict = {'verbosity': 0, 'extra_vars': obj.extra_vars}
1031 0
        return defaults_dict
1032

1033 3
    def get_job_template_data(self, obj):
1034 0
        return dict(name=obj.name, id=obj.id, description=obj.description)
1035

1036 3
    def validate_extra_vars(self, value):
1037 0
        return vars_validate_or_raise(value)
1038

1039

1040 3
class PolicyCalendarSerializer(EmptySerializer):
1041 3
    events = serializers.ListField(child=serializers.DateTimeField())
1042

1043

1044 3
class PolicyVMModuleSerializer(EmptySerializer):
1045 3
    modules = serializers.SerializerMethodField()
1046

1047

1048 3
class PolicyModuleSerializer(EmptySerializer):
1049 3
    modules = serializers.SerializerMethodField()
1050

1051

1052 3
class RestoreLaunchSerializer(BaseSerializer):
1053 3
    defaults = serializers.SerializerMethodField()
1054 3
    archive_name = serializers.CharField(required=True, write_only=True)
1055 3
    destination = serializers.CharField(required=True, write_only=True)
1056 3
    dest_folder = serializers.CharField(required=True, write_only=True)
1057 3
    dry_run = serializers.BooleanField(required=False, initial=False, write_only=True)
1058 3
    item = serializers.CharField(required=False, write_only=True)
1059 3
    verbosity = serializers.IntegerField(required=False, initial=0, min_value=0, max_value=4, write_only=True)
1060

1061 3
    class Meta:
1062 3
        model = Job
1063 3
        fields = ('defaults', 'archive_name', 'destination', 'dest_folder', 'dry_run', 'item', 'verbosity')
1064

1065 3
    def get_defaults(self, obj):
1066 0
        defaults_dict = {'verbosity': 0, 'archive_name': '', 'destination': '', 'dest_folder': '', 'dry_run': False, 'item': ''}
1067 0
        return defaults_dict
1068

1069 3
    def get_job_template_data(self, obj):
1070 0
        return dict(name=obj.name, id=obj.id, description=obj.description)
1071

1072 3
    def validate_extra_vars(self, value):
1073 0
        return vars_validate_or_raise(value)
1074

1075

1076 3
class CatalogSerializer(BaseSerializer):
1077

1078 3
    class Meta:
1079 3
        model = Catalog
1080 3
        fields = ('id', 'url', 'archive_name', 'path', 'job', 'mode', 'mtime', 'owner', 'group', 'size', 'healthy')
1081

1082 3
    def get_related(self, obj):
1083 0
        res = super(CatalogSerializer, self).get_related(obj)
1084 0
        if obj.job:
1085 0
            res['job'] = self.reverse('api:job_detail', kwargs={'pk': obj.job.pk})
1086 0
        return res
1087

1088 3
    def to_representation(self, obj):
1089 0
        ret = super(CatalogSerializer, self).to_representation(obj)
1090 0
        if obj is not None and 'job' in ret and not obj.job:
1091 0
            ret['job'] = None
1092 0
        return ret
1093

1094

1095 3
class CatalogListSerializer(DynamicFieldsSerializerMixin, CatalogSerializer):
1096

1097 3
    class Meta:
1098 3
        model = Catalog
1099 3
        fields = ('id', 'url', 'archive_name', 'path', 'job', 'mode', 'mtime', 'owner', 'group', 'size', 'healthy')
1100

1101

1102 3
class StatsSerializer(EmptySerializer):
1103 3
    stats = serializers.ListField(serializers.SerializerMethodField())
1104

1105

1106 3
class CyborgTokenObtainPairSerializer(TokenObtainPairSerializer):
1107 3
    @classmethod
1108
    def get_token(cls, user):
1109 0
        token = super().get_token(user)
1110
        # Add custom claims
1111 0
        token['email'] = user.email
1112 0
        token['first_name'] = user.first_name
1113 0
        token['last_name'] = user.last_name
1114

1115 0
        return token

Read our documentation on viewing source code .

Loading