1
# Python
2 3
import copy
3 3
import json
4 3
import re
5 3
import six
6 3
import urllib
7

8
# Python
9 3
from urllib.parse import urlparse
10 3
from collections import OrderedDict
11

12
# Django
13 3
from django.core.validators import URLValidator
14 3
from django.utils.translation import ugettext_lazy as _
15

16
# Django REST Framework
17 3
from rest_framework.fields import CharField, IntegerField, ListField, NullBooleanField, DictField
18

19 3
from jinja2 import Environment, StrictUndefined
20 3
from jinja2.exceptions import UndefinedError
21

22
# Django
23 3
from django.core import exceptions as django_exceptions
24 3
from django.db import models
25 3
from django.db.models.fields.related_descriptors import ReverseOneToOneDescriptor
26

27
# jsonschema
28 3
from jsonschema import Draft4Validator, FormatChecker
29 3
import jsonschema.exceptions
30

31
# Django-JSONField
32 3
from jsonfield import JSONField as upstream_JSONField
33 3
from jsonbfield.fields import JSONField as upstream_JSONBField
34

35
# DRF
36 3
from rest_framework import serializers
37

38
# CyBorgBackup
39 3
from cyborgbackup.main.utils.filters import SmartFilter
40 3
from cyborgbackup.main.validators import validate_ssh_private_key
41 3
from cyborgbackup.main import utils
42

43

44
# Provide a (better) custom error message for enum jsonschema validation
45 3
def __enum_validate__(validator, enums, instance, schema):
46 0
    if instance not in enums:
47 0
        yield jsonschema.exceptions.ValidationError(
48
            _("'%s' is not one of ['%s']") % (instance, "', '".join(enums))
49
        )
50

51

52 3
Draft4Validator.VALIDATORS['enum'] = __enum_validate__
53

54

55 3
class CharField(CharField):
56

57 3
    def to_representation(self, value):
58 0
        if value is None:
59 0
            return None
60 0
        return super(CharField, self).to_representation(value)
61

62

63 3
class IntegerField(IntegerField):
64

65 3
    def get_value(self, dictionary):
66 0
        ret = super(IntegerField, self).get_value(dictionary)
67
        # Handle UI corner case
68 0
        if ret == '' and self.allow_null and not getattr(self, 'allow_blank', False):
69 0
            return None
70 0
        return ret
71

72

73 3
class StringListField(ListField):
74

75 3
    child = CharField()
76

77 3
    def to_representation(self, value):
78 0
        if value is None and self.allow_null:
79 0
            return None
80 0
        return super(StringListField, self).to_representation(value)
81

82

83 3
class StringListBooleanField(ListField):
84

85 3
    default_error_messages = {
86
        'type_error': _('Expected None, True, False, a string or list of strings but got {input_type} instead.'),
87
    }
88 3
    child = CharField()
89

90 3
    def to_representation(self, value):
91 0
        try:
92 0
            if isinstance(value, (list, tuple)):
93 0
                return super(StringListBooleanField, self).to_representation(value)
94 0
            elif value in NullBooleanField.TRUE_VALUES:
95 0
                return True
96 0
            elif value in NullBooleanField.FALSE_VALUES:
97 0
                return False
98 0
            elif value in NullBooleanField.NULL_VALUES:
99 0
                return None
100 0
            elif isinstance(value, basestring):
101 0
                return self.child.to_representation(value)
102 0
        except TypeError:
103 0
            pass
104

105 0
        self.fail('type_error', input_type=type(value))
106

107 3
    def to_internal_value(self, data):
108 0
        try:
109 0
            if isinstance(data, (list, tuple)):
110 0
                return super(StringListBooleanField, self).to_internal_value(data)
111 0
            elif data in NullBooleanField.TRUE_VALUES:
112 0
                return True
113 0
            elif data in NullBooleanField.FALSE_VALUES:
114 0
                return False
115 0
            elif data in NullBooleanField.NULL_VALUES:
116 0
                return None
117 0
            elif isinstance(data, basestring):
118 0
                return self.child.run_validation(data)
119 0
        except TypeError:
120 0
            pass
121 0
        self.fail('type_error', input_type=type(data))
122

123

124 3
class URLField(CharField):
125

126 3
    def __init__(self, **kwargs):
127 0
        schemes = kwargs.pop('schemes', None)
128 0
        self.allow_plain_hostname = kwargs.pop('allow_plain_hostname', False)
129 0
        super(URLField, self).__init__(**kwargs)
130 0
        validator_kwargs = dict(message=_('Enter a valid URL'))
131 0
        if schemes is not None:
132 0
            validator_kwargs['schemes'] = schemes
133 0
        self.validators.append(URLValidator(**validator_kwargs))
134

135 3
    def to_representation(self, value):
136 0
        if value is None:
137 0
            return ''
138 0
        return super(URLField, self).to_representation(value)
139

140 3
    def run_validators(self, value):
141 0
        if self.allow_plain_hostname:
142 0
            try:
143 0
                url_parts = urlparse.urlsplit(value)
144 0
                if url_parts.hostname and '.' not in url_parts.hostname:
145 0
                    netloc = '{}.local'.format(url_parts.hostname)
146 0
                    if url_parts.port:
147 0
                        netloc = '{}:{}'.format(netloc, url_parts.port)
148 0
                    if url_parts.username:
149 0
                        if url_parts.password:
150 0
                            netloc = '{}:{}@{}' % (url_parts.username, url_parts.password, netloc)
151
                        else:
152 0
                            netloc = '{}@{}' % (url_parts.username, netloc)
153 0
                    value = urlparse.urlunsplit([url_parts.scheme,
154
                                                 netloc,
155
                                                 url_parts.path,
156
                                                 url_parts.query,
157
                                                 url_parts.fragment])
158 0
            except Exception:
159 0
                raise  # If something fails here, just fall through and let the validators check it.
160 0
        super(URLField, self).run_validators(value)
161

162

163 3
class KeyValueField(DictField):
164 3
    child = CharField()
165 3
    default_error_messages = {
166
        'invalid_child': _('"{input}" is not a valid string.')
167
    }
168

169 3
    def to_internal_value(self, data):
170 0
        ret = super(KeyValueField, self).to_internal_value(data)
171 0
        for value in data.values():
172 0
            if not isinstance(value, six.string_types + six.integer_types + (float,)):
173 0
                if isinstance(value, OrderedDict):
174 0
                    value = dict(value)
175 0
                self.fail('invalid_child', input=value)
176 0
        return ret
177

178

179 3
class ListTuplesField(ListField):
180 3
    default_error_messages = {
181
        'type_error': _('Expected a list of tuples of max length 2 but got {input_type} instead.'),
182
    }
183

184 3
    def to_representation(self, value):
185 0
        if isinstance(value, (list, tuple)):
186 0
            return super(ListTuplesField, self).to_representation(value)
187
        else:
188 0
            self.fail('type_error', input_type=type(value))
189

190 3
    def to_internal_value(self, data):
191 0
        if isinstance(data, list):
192 0
            for x in data:
193 0
                if not isinstance(x, (list, tuple)) or len(x) > 2:
194 0
                    self.fail('type_error', input_type=type(x))
195

196 0
            return super(ListTuplesField, self).to_internal_value(data)
197
        else:
198 0
            self.fail('type_error', input_type=type(data))
199

200

201 3
class JSONField(upstream_JSONField):
202

203 3
    def db_type(self, connection):
204 3
        return 'text'
205

206 3
    def _get_val_from_obj(self, obj):
207 0
        return self.value_from_object(obj)
208

209 3
    def from_db_value(self, value, expression, connection, context):
210 0
        if value in {'', None} and not self.null:
211 0
            return {}
212 0
        if isinstance(value, six.string_types):
213 0
            return json.loads(value)
214 0
        return value
215

216

217 3
class JSONBField(upstream_JSONBField):
218 3
    def get_prep_lookup(self, lookup_type, value):
219 0
        if isinstance(value, six.string_types) and value == "null":
220 0
            return 'null'
221 0
        return super(JSONBField, self).get_prep_lookup(lookup_type, value)
222

223 3
    def get_db_prep_value(self, value, connection, prepared=False):
224 0
        if connection.vendor == 'sqlite':
225
            # sqlite (which we use for tests) does not support jsonb;
226 0
            return json.dumps(value)
227 0
        return super(JSONBField, self).get_db_prep_value(
228
            value, connection, prepared
229
        )
230

231 3
    def from_db_value(self, value, expression, connection, context):
232
        # Work around a bug in django-jsonfield
233
        # https://bitbucket.org/schinckel/django-jsonfield/issues/57/cannot-use-in-the-same-project-as-djangos
234 0
        if isinstance(value, six.string_types):
235 0
            return json.loads(value)
236 0
        return value
237

238
# Based on AutoOneToOneField from django-annoying:
239
# https://bitbucket.org/offline/django-annoying/src/a0de8b294db3/annoying/fields.py
240

241

242 3
class AutoSingleRelatedObjectDescriptor(ReverseOneToOneDescriptor):
243
    """Descriptor for access to the object from its related class."""
244

245 3
    def __get__(self, instance, instance_type=None):
246 0
        try:
247 0
            return super(AutoSingleRelatedObjectDescriptor,
248
                         self).__get__(instance, instance_type)
249 0
        except self.related.related_model.DoesNotExist:
250 0
            obj = self.related.related_model(**{self.related.field.name: instance})
251 0
            if self.related.field.rel.parent_link:
252 0
                raise NotImplementedError('not supported with polymorphic!')
253 0
                for f in instance._meta.local_fields:
254 0
                    setattr(obj, f.name, getattr(instance, f.name))
255 0
            obj.save()
256 0
            return obj
257

258

259 3
class AutoOneToOneField(models.OneToOneField):
260
    """OneToOneField that creates related object if it doesn't exist."""
261

262 3
    def contribute_to_related_class(self, cls, related):
263 0
        setattr(cls, related.get_accessor_name(),
264
                AutoSingleRelatedObjectDescriptor(related))
265

266

267 3
class SmartFilterField(models.TextField):
268 3
    def get_prep_value(self, value):
269
        # Change any false value to none.
270
        # https://docs.python.org/2/library/stdtypes.html#truth-value-testing
271 0
        if not value:
272 0
            return None
273 0
        value = urllib.unquote(value)
274 0
        try:
275 0
            SmartFilter().query_from_string(value)
276 0
        except RuntimeError as e:
277 0
            raise models.base.ValidationError(e)
278 0
        return super(SmartFilterField, self).get_prep_value(value)
279

280

281 3
class JSONSchemaField(JSONBField):
282
    """
283
    A JSONB field that self-validates against a defined JSON schema
284
    (http://json-schema.org).  This base class is intended to be overwritten by
285
    defining `self.schema`.
286
    """
287

288 3
    format_checker = FormatChecker()
289

290
    # If an empty {} is provided, we still want to perform this schema
291
    # validation
292 3
    empty_values = (None, '')
293

294 3
    def get_default(self):
295 0
        return copy.deepcopy(super(JSONBField, self).get_default())
296

297 3
    def schema(self, model_instance):
298 0
        raise NotImplementedError()
299

300 3
    def validate(self, value, model_instance):
301 0
        super(JSONSchemaField, self).validate(value, model_instance)
302 0
        errors = []
303 0
        for error in Draft4Validator(
304
                self.schema(model_instance),
305
                format_checker=self.format_checker
306
        ).iter_errors(value):
307
            # strip Python unicode markers from jsonschema validation errors
308 0
            error.message = re.sub(r'\bu(\'|")', r'\1', error.message)
309

310 0
            if error.validator == 'pattern' and 'error' in error.schema:
311 0
                error.message = error.schema['error'] % error.instance
312 0
            errors.append(error)
313

314 0
        if errors:
315 0
            raise django_exceptions.ValidationError(
316
                [e.message for e in errors],
317
                code='invalid',
318
                params={'value': value},
319
            )
320

321 3
    def get_db_prep_value(self, value, connection, prepared=False):
322 0
        if connection.vendor == 'sqlite':
323
            # sqlite (which we use for tests) does not support jsonb;
324 0
            return json.dumps(value)
325 0
        return super(JSONSchemaField, self).get_db_prep_value(
326
            value, connection, prepared
327
        )
328

329 3
    def from_db_value(self, value, expression, connection, context):
330
        # Work around a bug in django-jsonfield
331
        # https://bitbucket.org/schinckel/django-jsonfield/issues/57/cannot-use-in-the-same-project-as-djangos
332 0
        if isinstance(value, six.string_types):
333 0
            return json.loads(value)
334 0
        return value
335

336

337 3
@JSONSchemaField.format_checker.checks('vault_id')
338
def format_vault_id(value):
339 0
    if '@' in value:
340 0
        raise jsonschema.exceptions.FormatError('@ is not an allowed character')
341 0
    return True
342

343

344 3
@JSONSchemaField.format_checker.checks('ssh_private_key')
345
def format_ssh_private_key(value):
346
    # Sanity check: GCE, in particular, provides JSON-encoded private
347
    # keys, which developers will be tempted to copy and paste rather
348
    # than JSON decode.
349
    #
350
    # These end in a unicode-encoded final character that gets double
351
    # escaped due to being in a Python 2 bytestring, and that causes
352
    # Python's key parsing to barf. Detect this issue and correct it.
353 0
    if not value or value == '$encrypted$':
354 0
        return True
355 0
    if r'\u003d' in value:
356 0
        value = value.replace(r'\u003d', '=')
357 0
    try:
358 0
        validate_ssh_private_key(value)
359 0
    except django_exceptions.ValidationError as e:
360 0
        raise jsonschema.exceptions.FormatError(e.message)
361 0
    return True
362

363

364 3
class CredentialInputField(JSONSchemaField):
365
    """
366
    Used to validate JSON for
367
    `cyborgbackup.main.models.credential:Credential().inputs`.
368

369
    Input data for credentials is represented as a dictionary e.g.,
370
    {'api_token': 'abc123', 'api_secret': 'SECRET'}
371

372
    For the data to be valid, the keys of this dictionary should correspond
373
    with the field names (and datatypes) defined in the associated
374
    CredentialType e.g.,
375

376
    {
377
        'fields': [{
378
            'id': 'api_token',
379
            'label': 'API Token',
380
            'type': 'string'
381
        }, {
382
            'id': 'api_secret',
383
            'label': 'API Secret',
384
            'type': 'string'
385
        }]
386
    }
387
    """
388

389 3
    def schema(self, model_instance):
390
        # determine the defined fields for the associated credential type
391 0
        properties = {}
392 0
        for field in model_instance.credential_type.inputs.get('fields', []):
393 0
            field = field.copy()
394 0
            properties[field['id']] = field
395 0
            if field.get('choices', []):
396 0
                field['enum'] = field['choices'][:]
397 0
        return {
398
            'type': 'object',
399
            'properties': properties,
400
            'dependencies': model_instance.credential_type.inputs.get('dependencies', {}),
401
            'additionalProperties': False,
402
        }
403

404 3
    def validate(self, value, model_instance):
405
        # decrypt secret values so we can validate their contents (i.e.,
406
        # ssh_key_data format)
407

408 0
        if not isinstance(value, dict):
409 0
            return super(CredentialInputField, self).validate(value,
410
                                                              model_instance)
411

412
        # Backwards compatability: in prior versions, if you submit `null` for
413
        # a credential field value, it just considers the value an empty string
414 0
        for unset in [key for key, v in model_instance.inputs.items() if not v]:
415 0
            default_value = model_instance.credential_type.default_for_field(unset)
416 0
            if default_value is not None:
417 0
                model_instance.inputs[unset] = default_value
418

419 0
        decrypted_values = {}
420 0
        for k, v in value.items():
421 0
            if all([
422
                    k in model_instance.credential_type.secret_fields,
423
                    v != '$encrypted$',
424
                    model_instance.pk
425
            ]):
426 0
                if not isinstance(getattr(model_instance, k), six.string_types):
427 0
                    raise django_exceptions.ValidationError(
428
                        _('secret values must be of type string, not {}').format(type(v).__name__),
429
                        code='invalid',
430
                        params={'value': v},
431
                    )
432 0
                decrypted_values[k] = utils.decrypt_field(model_instance, k)
433
            else:
434 0
                decrypted_values[k] = v
435

436 0
        super(JSONSchemaField, self).validate(decrypted_values, model_instance)
437 0
        errors = {}
438 0
        for error in Draft4Validator(
439
                self.schema(model_instance),
440
                format_checker=self.format_checker
441
        ).iter_errors(decrypted_values):
442 0
            if error.validator == 'pattern' and 'error' in error.schema:
443 0
                error.message = error.schema['error'] % error.instance
444 0
            if error.validator == 'dependencies':
445
                # replace the default error messaging w/ a better i18n string
446
                # I wish there was a better way to determine the parameters of
447
                # this validation failure, but the exception jsonschema raises
448
                # doesn't include them as attributes (just a hard-coded error
449
                # string)
450 0
                match = re.search(
451
                    # 'foo' is a dependency of 'bar'
452
                    r"'"         # apostrophe
453
                    r"([^']+)"   # one or more non-apostrophes (first group)
454
                    r"'[\w ]+'"  # one or more words/spaces
455
                    r"([^']+)",  # second group
456
                    error.message,
457
                )
458 0
                if match:
459 0
                    label, extraneous = match.groups()
460 0
                    if error.schema['properties'].get(label):
461 0
                        label = error.schema['properties'][label]['label']
462 0
                    errors[extraneous] = [
463
                        _('cannot be set unless "%s" is set') % label
464
                    ]
465 0
                    continue
466 0
            if 'id' not in error.schema:
467
                # If the error is not for a specific field, it's specific to
468
                # `inputs` in general
469 0
                raise django_exceptions.ValidationError(
470
                    error.message,
471
                    code='invalid',
472
                    params={'value': value},
473
                )
474 0
            errors[error.schema['id']] = [error.message]
475

476 0
        inputs = model_instance.credential_type.inputs
477 0
        for field in inputs.get('required', []):
478 0
            if not value.get(field, None):
479 0
                errors[field] = [_('required for %s') % (
480
                    model_instance.credential_type.name
481
                )]
482

483
        # `ssh_key_unlock` requirements are very specific and can't be
484
        # represented without complicated JSON schema
485 0
        if (
486
                model_instance.credential_type.managed_by_cyborgbackup is True and
487
                'ssh_key_unlock' in model_instance.credential_type.defined_fields
488
        ):
489

490
            # in order to properly test the necessity of `ssh_key_unlock`, we
491
            # need to know the real value of `ssh_key_data`; for a payload like:
492
            # {
493
            #   'ssh_key_data': '$encrypted$',
494
            #   'ssh_key_unlock': 'do-you-need-me?',
495
            # }
496
            # ...we have to fetch the actual key value from the database
497 0
            if model_instance.pk and model_instance.ssh_key_data == '$encrypted$':
498 0
                model_instance.ssh_key_data = model_instance.__class__.objects.get(
499
                    pk=model_instance.pk
500
                ).ssh_key_data
501

502 0
            if model_instance.has_encrypted_ssh_key_data and not value.get('ssh_key_unlock'):
503 0
                errors['ssh_key_unlock'] = [_('must be set when SSH key is encrypted.')]
504 0
            if all([
505
                    model_instance.ssh_key_data,
506
                    value.get('ssh_key_unlock'),
507
                    not model_instance.has_encrypted_ssh_key_data
508
            ]):
509 0
                errors['ssh_key_unlock'] = [_('should not be set when SSH key is not encrypted.')]
510

511 0
        if errors:
512 0
            raise serializers.ValidationError({
513
                'inputs': errors
514
            })
515

516

517 3
class CredentialTypeInputField(JSONSchemaField):
518
    """
519
    Used to validate JSON for
520
    `cyborgbackup.main.models.credential:CredentialType().inputs`.
521
    """
522

523 3
    def schema(self, model_instance):
524 0
        return {
525
            'type': 'object',
526
            'additionalProperties': False,
527
            'properties': {
528
                'required': {
529
                    'type': 'array',
530
                    'items': {'type': 'string'}
531
                },
532
                'fields':  {
533
                    'type': 'array',
534
                    'items': {
535
                        'type': 'object',
536
                        'properties': {
537
                            'type': {'enum': ['string', 'boolean']},
538
                            'format': {'enum': ['ssh_private_key']},
539
                            'choices': {
540
                                'type': 'array',
541
                                'minItems': 1,
542
                                'items': {'type': 'string'},
543
                                'uniqueItems': True
544
                            },
545
                            'id': {
546
                                'type': 'string',
547
                                'pattern': '^[a-zA-Z_]+[a-zA-Z0-9_]*$',
548
                                'error': '%s is an invalid variable name',
549
                            },
550
                            'label': {'type': 'string'},
551
                            'help_text': {'type': 'string'},
552
                            'multiline': {'type': 'boolean'},
553
                            'secret': {'type': 'boolean'},
554
                            'ask_at_runtime': {'type': 'boolean'},
555
                        },
556
                        'additionalProperties': False,
557
                        'required': ['id', 'label'],
558
                    }
559
                }
560
            }
561
        }
562

563 3
    def validate(self, value, model_instance):
564 0
        if isinstance(value, dict) and 'dependencies' in value and \
565
                not model_instance.managed_by_cyborgbackup:
566 0
            raise django_exceptions.ValidationError(
567
                _("'dependencies' is not supported for custom credentials."),
568
                code='invalid',
569
                params={'value': value},
570
            )
571

572 0
        super(CredentialTypeInputField, self).validate(
573
            value, model_instance
574
        )
575

576 0
        ids = {}
577 0
        for field in value.get('fields', []):
578 0
            id_ = field.get('id')
579 0
            if id_ == 'cyborgbackup':
580 0
                raise django_exceptions.ValidationError(
581
                    _('"cyborgbackup" is a reserved field name'),
582
                    code='invalid',
583
                    params={'value': value},
584
                )
585

586 0
            if id_ in ids:
587 0
                raise django_exceptions.ValidationError(
588
                    _('field IDs must be unique (%s)' % id_),
589
                    code='invalid',
590
                    params={'value': value},
591
                )
592 0
            ids[id_] = True
593

594 0
            if 'type' not in field:
595
                # If no type is specified, default to string
596 0
                field['type'] = 'string'
597

598 0
            for key in ('choices', 'multiline', 'format', 'secret',):
599 0
                if key in field and field['type'] != 'string':
600 0
                    raise django_exceptions.ValidationError(
601
                        _('%s not allowed for %s type (%s)' % (key, field['type'], field['id'])),
602
                        code='invalid',
603
                        params={'value': value},
604
                    )
605

606

607 3
class CredentialTypeInjectorField(JSONSchemaField):
608
    """
609
    Used to validate JSON for
610
    `cyborgbackup.main.models.credential:CredentialType().injectors`.
611
    """
612

613 3
    def schema(self, model_instance):
614 0
        return {
615
            'type': 'object',
616
            'additionalProperties': False,
617
            'properties': {
618
                'file': {
619
                    'type': 'object',
620
                    'patternProperties': {
621
                        r'^template(\.[a-zA-Z_]+[a-zA-Z0-9_]*)?$': {'type': 'string'},
622
                    },
623
                    'additionalProperties': False,
624
                },
625
                'env': {
626
                    'type': 'object',
627
                    'patternProperties': {
628
                        # http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
629
                        # In the shell command language, a word consisting solely
630
                        # of underscores, digits, and alphabetics from the portable
631
                        # character set. The first character of a name is not
632
                        # a digit.
633
                        r'^[a-zA-Z_]+[a-zA-Z0-9_]*$': {'type': 'string'},
634
                    },
635
                    'additionalProperties': False,
636
                },
637
                'extra_vars': {
638
                    'type': 'object',
639
                    'patternProperties': {
640
                        r'^[a-zA-Z_]+[a-zA-Z0-9_]*$': {'type': 'string'},
641
                    },
642
                    'additionalProperties': False,
643
                },
644
            },
645
            'additionalProperties': False
646
        }
647

648 3
    def validate(self, value, model_instance):
649 0
        super(CredentialTypeInjectorField, self).validate(
650
            value, model_instance
651
        )
652

653
        # make sure the inputs are valid first
654 0
        try:
655 0
            CredentialTypeInputField().validate(model_instance.inputs, model_instance)
656 0
        except django_exceptions.ValidationError:
657
            # If `model_instance.inputs` itself is invalid, we can't make an
658
            # estimation as to whether our Jinja templates contain valid field
659
            # names; don't continue
660 0
            return
661

662
        # In addition to basic schema validation, search the injector fields
663
        # for template variables and make sure they match the fields defined in
664
        # the inputs
665 0
        valid_namespace = dict(
666
            (field, 'EXAMPLE')
667
            for field in model_instance.defined_fields
668
        )
669

670 0
        class MilkyprovisionNamespace:
671 0
            filename = None
672 0
        valid_namespace['cyborgbackup'] = MilkyprovisionNamespace()
673

674
        # ensure either single file or multi-file syntax is used (but not both)
675 0
        template_names = [x for x in value.get('file', {}).keys() if x.startswith('template')]
676 0
        if 'template' in template_names and len(template_names) > 1:
677 0
            raise django_exceptions.ValidationError(
678
                _('Must use multi-file syntax when injecting multiple files'),
679
                code='invalid',
680
                params={'value': value},
681
            )
682 0
        if 'template' not in template_names:
683 0
            valid_namespace['cyborgbackup'].filename = MilkyprovisionNamespace()
684 0
            for template_name in template_names:
685 0
                template_name = template_name.split('.')[1]
686 0
                setattr(valid_namespace['cyborgbackup'].filename, template_name, 'EXAMPLE')
687

688 0
        for type_, injector in value.items():
689 0
            for key, tmpl in injector.items():
690 0
                try:
691 0
                    Environment(
692
                        undefined=StrictUndefined
693
                    ).from_string(tmpl).render(valid_namespace)
694 0
                except UndefinedError as e:
695 0
                    raise django_exceptions.ValidationError(
696
                        _('%s uses an undefined field (%s)') % (key, e),
697
                        code='invalid',
698
                        params={'value': value},
699
                    )
700

701

702 3
class AskForField(models.BooleanField):
703
    """
704
    Denotes whether to prompt on launch for another field on the same template
705
    """
706 3
    def __init__(self, allows_field=None, **kwargs):
707 0
        super(AskForField, self).__init__(**kwargs)
708 0
        self._allows_field = allows_field
709

710 3
    @property
711
    def allows_field(self):
712 0
        if self._allows_field is None:
713 0
            try:
714 0
                return self.name[len('ask_'):-len('_on_launch')]
715 0
            except AttributeError:
716
                # self.name will be set by the model metaclass, not this field
717 0
                raise Exception('Corresponding allows_field cannot be accessed until model is initialized.')
718 0
        return self._allows_field

Read our documentation on viewing source code .

Loading