• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bagerard / mongoengine / 6391040345

03 Oct 2023 09:18AM UTC coverage: 94.46% (+0.4%) from 94.046%
6391040345

push

github

bagerard
Merge branch 'master' of github.com:MongoEngine/mongoengine

61 of 61 new or added lines in 9 files covered. (100.0%)

5132 of 5433 relevant lines covered (94.46%)

1.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.92
/mongoengine/fields.py
1
import datetime
2✔
2
import decimal
2✔
3
import inspect
2✔
4
import itertools
2✔
5
import re
2✔
6
import socket
2✔
7
import time
2✔
8
import uuid
2✔
9
from io import BytesIO
2✔
10
from operator import itemgetter
2✔
11

12
import gridfs
2✔
13
import pymongo
2✔
14
from bson import SON, Binary, DBRef, ObjectId
2✔
15
from bson.decimal128 import Decimal128, create_decimal128_context
2✔
16
from bson.int64 import Int64
2✔
17
from pymongo import ReturnDocument
2✔
18

19
try:
2✔
20
    import dateutil
2✔
21
except ImportError:
2✔
22
    dateutil = None
2✔
23
else:
24
    import dateutil.parser
×
25

26
from mongoengine.base import (
2✔
27
    BaseDocument,
28
    BaseField,
29
    ComplexBaseField,
30
    GeoJsonBaseField,
31
    LazyReference,
32
    ObjectIdField,
33
    get_document,
34
)
35
from mongoengine.base.utils import LazyRegexCompiler
2✔
36
from mongoengine.common import _import_class
2✔
37
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
2✔
38
from mongoengine.document import Document, EmbeddedDocument
2✔
39
from mongoengine.errors import (
2✔
40
    DoesNotExist,
41
    InvalidQueryError,
42
    ValidationError,
43
)
44
from mongoengine.queryset import DO_NOTHING
2✔
45
from mongoengine.queryset.base import BaseQuerySet
2✔
46
from mongoengine.queryset.transform import STRING_OPERATORS
2✔
47

48
try:
2✔
49
    from PIL import Image, ImageOps
2✔
50

51
    LANCZOS = Image.LANCZOS if hasattr(Image, "LANCZOS") else Image.ANTIALIAS
2✔
52
except ImportError:
×
53
    Image = None
×
54
    ImageOps = None
×
55

56

57
__all__ = (
2✔
58
    "StringField",
59
    "URLField",
60
    "EmailField",
61
    "IntField",
62
    "LongField",
63
    "FloatField",
64
    "DecimalField",
65
    "BooleanField",
66
    "DateTimeField",
67
    "DateField",
68
    "ComplexDateTimeField",
69
    "EmbeddedDocumentField",
70
    "ObjectIdField",
71
    "GenericEmbeddedDocumentField",
72
    "DynamicField",
73
    "ListField",
74
    "SortedListField",
75
    "EmbeddedDocumentListField",
76
    "DictField",
77
    "MapField",
78
    "ReferenceField",
79
    "CachedReferenceField",
80
    "LazyReferenceField",
81
    "GenericLazyReferenceField",
82
    "GenericReferenceField",
83
    "BinaryField",
84
    "GridFSError",
85
    "GridFSProxy",
86
    "FileField",
87
    "ImageGridFsProxy",
88
    "ImproperlyConfigured",
89
    "ImageField",
90
    "GeoPointField",
91
    "PointField",
92
    "LineStringField",
93
    "PolygonField",
94
    "SequenceField",
95
    "UUIDField",
96
    "EnumField",
97
    "MultiPointField",
98
    "MultiLineStringField",
99
    "MultiPolygonField",
100
    "GeoJsonBaseField",
101
    "Decimal128Field",
102
)
103

104
RECURSIVE_REFERENCE_CONSTANT = "self"
2✔
105

106

107
class StringField(BaseField):
2✔
108
    """A unicode string field."""
109

110
    def __init__(self, regex=None, max_length=None, min_length=None, **kwargs):
2✔
111
        """
112
        :param regex: (optional) A string pattern that will be applied during validation
113
        :param max_length: (optional) A max length that will be applied during validation
114
        :param min_length: (optional) A min length that will be applied during validation
115
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField`
116
        """
117
        self.regex = re.compile(regex) if regex else None
2✔
118
        self.max_length = max_length
2✔
119
        self.min_length = min_length
2✔
120
        super().__init__(**kwargs)
2✔
121

122
    def to_python(self, value):
2✔
123
        if isinstance(value, str):
2✔
124
            return value
2✔
125
        try:
2✔
126
            value = value.decode("utf-8")
2✔
127
        except Exception:
2✔
128
            pass
2✔
129
        return value
2✔
130

131
    def validate(self, value):
2✔
132
        if not isinstance(value, str):
2✔
133
            self.error("StringField only accepts string values")
2✔
134

135
        if self.max_length is not None and len(value) > self.max_length:
2✔
136
            self.error("String value is too long")
2✔
137

138
        if self.min_length is not None and len(value) < self.min_length:
2✔
139
            self.error("String value is too short")
2✔
140

141
        if self.regex is not None and self.regex.match(value) is None:
2✔
142
            self.error("String value did not match validation regex")
2✔
143

144
    def lookup_member(self, member_name):
2✔
145
        return None
2✔
146

147
    def prepare_query_value(self, op, value):
2✔
148
        if not isinstance(op, str):
2✔
149
            return value
2✔
150

151
        if op in STRING_OPERATORS:
2✔
152
            case_insensitive = op.startswith("i")
2✔
153
            op = op.lstrip("i")
2✔
154

155
            flags = re.IGNORECASE if case_insensitive else 0
2✔
156

157
            regex = r"%s"
2✔
158
            if op == "startswith":
2✔
159
                regex = r"^%s"
2✔
160
            elif op == "endswith":
2✔
161
                regex = r"%s$"
2✔
162
            elif op == "exact":
2✔
163
                regex = r"^%s$"
2✔
164
            elif op == "wholeword":
2✔
165
                regex = r"\b%s\b"
2✔
166
            elif op == "regex":
2✔
167
                regex = value
2✔
168

169
            if op == "regex":
2✔
170
                value = re.compile(regex, flags)
2✔
171
            else:
172
                # escape unsafe characters which could lead to a re.error
173
                value = re.escape(value)
2✔
174
                value = re.compile(regex % value, flags)
2✔
175
        return super().prepare_query_value(op, value)
2✔
176

177

178
class URLField(StringField):
2✔
179
    """A field that validates input as an URL."""
180

181
    _URL_REGEX = LazyRegexCompiler(
2✔
182
        r"^(?:[a-z0-9\.\-]*)://"  # scheme is validated separately
183
        r"(?:(?:[A-Z0-9](?:[A-Z0-9-_]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}(?<!-)\.?)|"  # domain...
184
        r"localhost|"  # localhost...
185
        r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|"  # ...or ipv4
186
        r"\[?[A-F0-9]*:[A-F0-9:]+\]?)"  # ...or ipv6
187
        r"(?::\d+)?"  # optional port
188
        r"(?:/?|[/?]\S+)$",
189
        re.IGNORECASE,
190
    )
191
    _URL_SCHEMES = ["http", "https", "ftp", "ftps"]
2✔
192

193
    def __init__(self, url_regex=None, schemes=None, **kwargs):
2✔
194
        """
195
        :param url_regex: (optional) Overwrite the default regex used for validation
196
        :param schemes: (optional) Overwrite the default URL schemes that are allowed
197
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.StringField`
198
        """
199
        self.url_regex = url_regex or self._URL_REGEX
2✔
200
        self.schemes = schemes or self._URL_SCHEMES
2✔
201
        super().__init__(**kwargs)
2✔
202

203
    def validate(self, value):
2✔
204
        # Check first if the scheme is valid
205
        scheme = value.split("://")[0].lower()
2✔
206
        if scheme not in self.schemes:
2✔
207
            self.error(f"Invalid scheme {scheme} in URL: {value}")
2✔
208

209
        # Then check full URL
210
        if not self.url_regex.match(value):
2✔
211
            self.error(f"Invalid URL: {value}")
2✔
212

213

214
class EmailField(StringField):
2✔
215
    """A field that validates input as an email address."""
216

217
    USER_REGEX = LazyRegexCompiler(
2✔
218
        # `dot-atom` defined in RFC 5322 Section 3.2.3.
219
        r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z"
220
        # `quoted-string` defined in RFC 5322 Section 3.2.4.
221
        r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"\Z)',
222
        re.IGNORECASE,
223
    )
224

225
    UTF8_USER_REGEX = LazyRegexCompiler(
2✔
226
        (
227
            # RFC 6531 Section 3.3 extends `atext` (used by dot-atom) to
228
            # include `UTF8-non-ascii`.
229
            r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z\u0080-\U0010FFFF]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z\u0080-\U0010FFFF]+)*\Z"
230
            # `quoted-string`
231
            r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"\Z)'
232
        ),
233
        re.IGNORECASE | re.UNICODE,
234
    )
235

236
    DOMAIN_REGEX = LazyRegexCompiler(
2✔
237
        r"((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)(?:[A-Z0-9-]{2,63}(?<!-))\Z",
238
        re.IGNORECASE,
239
    )
240

241
    error_msg = "Invalid email address: %s"
2✔
242

243
    def __init__(
2✔
244
        self,
245
        domain_whitelist=None,
246
        allow_utf8_user=False,
247
        allow_ip_domain=False,
248
        *args,
249
        **kwargs,
250
    ):
251
        """
252
        :param domain_whitelist: (optional) list of valid domain names applied during validation
253
        :param allow_utf8_user: Allow user part of the email to contain utf8 char
254
        :param allow_ip_domain: Allow domain part of the email to be an IPv4 or IPv6 address
255
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.StringField`
256
        """
257
        self.domain_whitelist = domain_whitelist or []
2✔
258
        self.allow_utf8_user = allow_utf8_user
2✔
259
        self.allow_ip_domain = allow_ip_domain
2✔
260
        super().__init__(*args, **kwargs)
2✔
261

262
    def validate_user_part(self, user_part):
2✔
263
        """Validate the user part of the email address. Return True if
264
        valid and False otherwise.
265
        """
266
        if self.allow_utf8_user:
2✔
267
            return self.UTF8_USER_REGEX.match(user_part)
2✔
268
        return self.USER_REGEX.match(user_part)
2✔
269

270
    def validate_domain_part(self, domain_part):
2✔
271
        """Validate the domain part of the email address. Return True if
272
        valid and False otherwise.
273
        """
274
        # Skip domain validation if it's in the whitelist.
275
        if domain_part in self.domain_whitelist:
2✔
276
            return True
2✔
277

278
        if self.DOMAIN_REGEX.match(domain_part):
2✔
279
            return True
2✔
280

281
        # Validate IPv4/IPv6, e.g. user@[192.168.0.1]
282
        if self.allow_ip_domain and domain_part[0] == "[" and domain_part[-1] == "]":
2✔
283
            for addr_family in (socket.AF_INET, socket.AF_INET6):
2✔
284
                try:
2✔
285
                    socket.inet_pton(addr_family, domain_part[1:-1])
2✔
286
                    return True
2✔
287
                except (OSError, UnicodeEncodeError):
2✔
288
                    pass
2✔
289

290
        return False
2✔
291

292
    def validate(self, value):
2✔
293
        super().validate(value)
2✔
294

295
        if "@" not in value:
2✔
296
            self.error(self.error_msg % value)
2✔
297

298
        user_part, domain_part = value.rsplit("@", 1)
2✔
299

300
        # Validate the user part.
301
        if not self.validate_user_part(user_part):
2✔
302
            self.error(self.error_msg % value)
2✔
303

304
        # Validate the domain and, if invalid, see if it's IDN-encoded.
305
        if not self.validate_domain_part(domain_part):
2✔
306
            try:
2✔
307
                domain_part = domain_part.encode("idna").decode("ascii")
2✔
308
            except UnicodeError:
2✔
309
                self.error(
2✔
310
                    "{} {}".format(
311
                        self.error_msg % value, "(domain failed IDN encoding)"
312
                    )
313
                )
314
            else:
315
                if not self.validate_domain_part(domain_part):
2✔
316
                    self.error(
2✔
317
                        "{} {}".format(
318
                            self.error_msg % value, "(domain validation failed)"
319
                        )
320
                    )
321

322

323
class IntField(BaseField):
2✔
324
    """32-bit integer field."""
325

326
    def __init__(self, min_value=None, max_value=None, **kwargs):
2✔
327
        """
328
        :param min_value: (optional) A min value that will be applied during validation
329
        :param max_value: (optional) A max value that will be applied during validation
330
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField`
331
        """
332
        self.min_value, self.max_value = min_value, max_value
2✔
333
        super().__init__(**kwargs)
2✔
334

335
    def to_python(self, value):
2✔
336
        try:
2✔
337
            value = int(value)
2✔
338
        except (TypeError, ValueError):
2✔
339
            pass
2✔
340
        return value
2✔
341

342
    def validate(self, value):
2✔
343
        try:
2✔
344
            value = int(value)
2✔
345
        except (TypeError, ValueError):
2✔
346
            self.error("%s could not be converted to int" % value)
2✔
347

348
        if self.min_value is not None and value < self.min_value:
2✔
349
            self.error("Integer value is too small")
2✔
350

351
        if self.max_value is not None and value > self.max_value:
2✔
352
            self.error("Integer value is too large")
2✔
353

354
    def prepare_query_value(self, op, value):
2✔
355
        if value is None:
2✔
356
            return value
2✔
357

358
        return super().prepare_query_value(op, int(value))
2✔
359

360

361
class LongField(IntField):
2✔
362
    """64-bit integer field. (Equivalent to IntField since the support to Python2 was dropped)"""
363

364
    def to_mongo(self, value):
2✔
365
        return Int64(value)
2✔
366

367

368
class FloatField(BaseField):
2✔
369
    """Floating point number field."""
370

371
    def __init__(self, min_value=None, max_value=None, **kwargs):
2✔
372
        """
373
        :param min_value: (optional) A min value that will be applied during validation
374
        :param max_value: (optional) A max value that will be applied during validation
375
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField`
376
        """
377
        self.min_value, self.max_value = min_value, max_value
2✔
378
        super().__init__(**kwargs)
2✔
379

380
    def to_python(self, value):
2✔
381
        try:
2✔
382
            value = float(value)
2✔
383
        except ValueError:
2✔
384
            pass
2✔
385
        return value
2✔
386

387
    def validate(self, value):
2✔
388
        if isinstance(value, int):
2✔
389
            try:
2✔
390
                value = float(value)
2✔
391
            except OverflowError:
2✔
392
                self.error("The value is too large to be converted to float")
2✔
393

394
        if not isinstance(value, float):
2✔
395
            self.error("FloatField only accepts float and integer values")
2✔
396

397
        if self.min_value is not None and value < self.min_value:
2✔
398
            self.error("Float value is too small")
2✔
399

400
        if self.max_value is not None and value > self.max_value:
2✔
401
            self.error("Float value is too large")
2✔
402

403
    def prepare_query_value(self, op, value):
2✔
404
        if value is None:
2✔
405
            return value
2✔
406

407
        return super().prepare_query_value(op, float(value))
2✔
408

409

410
class DecimalField(BaseField):
2✔
411
    """Disclaimer: This field is kept for historical reason but since it converts the values to float, it
412
    is not suitable for true decimal storage. Consider using :class:`~mongoengine.fields.Decimal128Field`.
413

414
    Fixed-point decimal number field. Stores the value as a float by default unless `force_string` is used.
415
    If using floats, beware of Decimal to float conversion (potential precision loss)
416
    """
417

418
    def __init__(
2✔
419
        self,
420
        min_value=None,
421
        max_value=None,
422
        force_string=False,
423
        precision=2,
424
        rounding=decimal.ROUND_HALF_UP,
425
        **kwargs,
426
    ):
427
        """
428
        :param min_value: (optional) A min value that will be applied during validation
429
        :param max_value: (optional) A max value that will be applied during validation
430
        :param force_string: Store the value as a string (instead of a float).
431
         Be aware that this affects query sorting and operation like lte, gte (as string comparison is applied)
432
         and some query operator won't work (e.g. inc, dec)
433
        :param precision: Number of decimal places to store.
434
        :param rounding: The rounding rule from the python decimal library:
435

436
            - decimal.ROUND_CEILING (towards Infinity)
437
            - decimal.ROUND_DOWN (towards zero)
438
            - decimal.ROUND_FLOOR (towards -Infinity)
439
            - decimal.ROUND_HALF_DOWN (to nearest with ties going towards zero)
440
            - decimal.ROUND_HALF_EVEN (to nearest with ties going to nearest even integer)
441
            - decimal.ROUND_HALF_UP (to nearest with ties going away from zero)
442
            - decimal.ROUND_UP (away from zero)
443
            - decimal.ROUND_05UP (away from zero if last digit after rounding towards zero would have been 0 or 5; otherwise towards zero)
444

445
            Defaults to: ``decimal.ROUND_HALF_UP``
446
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField`
447
        """
448
        self.min_value = min_value
2✔
449
        self.max_value = max_value
2✔
450
        self.force_string = force_string
2✔
451

452
        if precision < 0 or not isinstance(precision, int):
2✔
453
            self.error("precision must be a positive integer")
2✔
454

455
        self.precision = precision
2✔
456
        self.rounding = rounding
2✔
457

458
        super().__init__(**kwargs)
2✔
459

460
    def to_python(self, value):
2✔
461
        # Convert to string for python 2.6 before casting to Decimal
462
        try:
2✔
463
            value = decimal.Decimal("%s" % value)
2✔
464
        except (TypeError, ValueError, decimal.InvalidOperation):
2✔
465
            return value
2✔
466
        if self.precision > 0:
2✔
467
            return value.quantize(
2✔
468
                decimal.Decimal(".%s" % ("0" * self.precision)), rounding=self.rounding
469
            )
470
        else:
471
            return value.quantize(decimal.Decimal(), rounding=self.rounding)
2✔
472

473
    def to_mongo(self, value):
2✔
474
        if self.force_string:
2✔
475
            return str(self.to_python(value))
2✔
476
        return float(self.to_python(value))
2✔
477

478
    def validate(self, value):
2✔
479
        if not isinstance(value, decimal.Decimal):
2✔
480
            if not isinstance(value, str):
2✔
481
                value = str(value)
2✔
482
            try:
2✔
483
                value = decimal.Decimal(value)
2✔
484
            except (TypeError, ValueError, decimal.InvalidOperation) as exc:
2✔
485
                self.error("Could not convert value to decimal: %s" % exc)
2✔
486

487
        if self.min_value is not None and value < self.min_value:
2✔
488
            self.error("Decimal value is too small")
2✔
489

490
        if self.max_value is not None and value > self.max_value:
2✔
491
            self.error("Decimal value is too large")
2✔
492

493
    def prepare_query_value(self, op, value):
2✔
494
        if value is None:
2✔
495
            return value
2✔
496
        return super().prepare_query_value(op, self.to_mongo(value))
2✔
497

498

499
class BooleanField(BaseField):
2✔
500
    """Boolean field type."""
501

502
    def to_python(self, value):
2✔
503
        try:
2✔
504
            value = bool(value)
2✔
505
        except (ValueError, TypeError):
2✔
506
            pass
2✔
507
        return value
2✔
508

509
    def validate(self, value):
2✔
510
        if not isinstance(value, bool):
2✔
511
            self.error("BooleanField only accepts boolean values")
2✔
512

513

514
class DateTimeField(BaseField):
2✔
515
    """Datetime field.
516

517
    Uses the python-dateutil library if available alternatively use time.strptime
518
    to parse the dates.  Note: python-dateutil's parser is fully featured and when
519
    installed you can utilise it to convert varying types of date formats into valid
520
    python datetime objects.
521

522
    Note: To default the field to the current datetime, use: DateTimeField(default=datetime.utcnow)
523

524
    Note: Microseconds are rounded to the nearest millisecond.
525
      Pre UTC microsecond support is effectively broken.
526
      Use :class:`~mongoengine.fields.ComplexDateTimeField` if you
527
      need accurate microsecond support.
528
    """
529

530
    def validate(self, value):
2✔
531
        new_value = self.to_mongo(value)
2✔
532
        if not isinstance(new_value, (datetime.datetime, datetime.date)):
2✔
533
            self.error('cannot parse date "%s"' % value)
2✔
534

535
    def to_mongo(self, value):
2✔
536
        if value is None:
2✔
537
            return value
2✔
538
        if isinstance(value, datetime.datetime):
2✔
539
            return value
2✔
540
        if isinstance(value, datetime.date):
2✔
541
            return datetime.datetime(value.year, value.month, value.day)
2✔
542
        if callable(value):
2✔
543
            return value()
2✔
544

545
        if isinstance(value, str):
2✔
546
            return self._parse_datetime(value)
2✔
547
        else:
548
            return None
2✔
549

550
    @staticmethod
2✔
551
    def _parse_datetime(value):
552
        # Attempt to parse a datetime from a string
553
        value = value.strip()
2✔
554
        if not value:
2✔
555
            return None
2✔
556

557
        if dateutil:
2✔
558
            try:
×
559
                return dateutil.parser.parse(value)
×
560
            except (TypeError, ValueError, OverflowError):
×
561
                return None
×
562

563
        # split usecs, because they are not recognized by strptime.
564
        if "." in value:
2✔
565
            try:
2✔
566
                value, usecs = value.split(".")
2✔
567
                usecs = int(usecs)
2✔
568
            except ValueError:
2✔
569
                return None
2✔
570
        else:
571
            usecs = 0
2✔
572
        kwargs = {"microsecond": usecs}
2✔
573
        try:  # Seconds are optional, so try converting seconds first.
2✔
574
            return datetime.datetime(
2✔
575
                *time.strptime(value, "%Y-%m-%d %H:%M:%S")[:6], **kwargs
576
            )
577
        except ValueError:
2✔
578
            try:  # Try without seconds.
2✔
579
                return datetime.datetime(
2✔
580
                    *time.strptime(value, "%Y-%m-%d %H:%M")[:5], **kwargs
581
                )
582
            except ValueError:  # Try without hour/minutes/seconds.
2✔
583
                try:
2✔
584
                    return datetime.datetime(
2✔
585
                        *time.strptime(value, "%Y-%m-%d")[:3], **kwargs
586
                    )
587
                except ValueError:
2✔
588
                    return None
2✔
589

590
    def prepare_query_value(self, op, value):
2✔
591
        return super().prepare_query_value(op, self.to_mongo(value))
2✔
592

593

594
class DateField(DateTimeField):
2✔
595
    def to_mongo(self, value):
2✔
596
        value = super().to_mongo(value)
2✔
597
        # drop hours, minutes, seconds
598
        if isinstance(value, datetime.datetime):
2✔
599
            value = datetime.datetime(value.year, value.month, value.day)
2✔
600
        return value
2✔
601

602
    def to_python(self, value):
2✔
603
        value = super().to_python(value)
2✔
604
        # convert datetime to date
605
        if isinstance(value, datetime.datetime):
2✔
606
            value = datetime.date(value.year, value.month, value.day)
2✔
607
        return value
2✔
608

609

610
class ComplexDateTimeField(StringField):
2✔
611
    """
612
    ComplexDateTimeField handles microseconds exactly instead of rounding
613
    like DateTimeField does.
614

615
    Derives from a StringField so you can do `gte` and `lte` filtering by
616
    using lexicographical comparison when filtering / sorting strings.
617

618
    The stored string has the following format:
619

620
        YYYY,MM,DD,HH,MM,SS,NNNNNN
621

622
    Where NNNNNN is the number of microseconds of the represented `datetime`.
623
    The `,` as the separator can be easily modified by passing the `separator`
624
    keyword when initializing the field.
625

626
    Note: To default the field to the current datetime, use: DateTimeField(default=datetime.utcnow)
627
    """
628

629
    def __init__(self, separator=",", **kwargs):
2✔
630
        """
631
        :param separator: Allows to customize the separator used for storage (default ``,``)
632
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.StringField`
633
        """
634
        self.separator = separator
2✔
635
        self.format = separator.join(["%Y", "%m", "%d", "%H", "%M", "%S", "%f"])
2✔
636
        super().__init__(**kwargs)
2✔
637

638
    def _convert_from_datetime(self, val):
2✔
639
        """
640
        Convert a `datetime` object to a string representation (which will be
641
        stored in MongoDB). This is the reverse function of
642
        `_convert_from_string`.
643

644
        >>> a = datetime(2011, 6, 8, 20, 26, 24, 92284)
645
        >>> ComplexDateTimeField()._convert_from_datetime(a)
646
        '2011,06,08,20,26,24,092284'
647
        """
648
        return val.strftime(self.format)
2✔
649

650
    def _convert_from_string(self, data):
2✔
651
        """
652
        Convert a string representation to a `datetime` object (the object you
653
        will manipulate). This is the reverse function of
654
        `_convert_from_datetime`.
655

656
        >>> a = '2011,06,08,20,26,24,092284'
657
        >>> ComplexDateTimeField()._convert_from_string(a)
658
        datetime.datetime(2011, 6, 8, 20, 26, 24, 92284)
659
        """
660
        values = [int(d) for d in data.split(self.separator)]
2✔
661
        return datetime.datetime(*values)
2✔
662

663
    def __get__(self, instance, owner):
2✔
664
        if instance is None:
2✔
665
            return self
×
666

667
        data = super().__get__(instance, owner)
2✔
668

669
        if isinstance(data, datetime.datetime) or data is None:
2✔
670
            return data
2✔
671
        return self._convert_from_string(data)
2✔
672

673
    def __set__(self, instance, value):
2✔
674
        super().__set__(instance, value)
2✔
675
        value = instance._data[self.name]
2✔
676
        if value is not None:
2✔
677
            if isinstance(value, datetime.datetime):
2✔
678
                instance._data[self.name] = self._convert_from_datetime(value)
2✔
679
            else:
680
                instance._data[self.name] = value
2✔
681

682
    def validate(self, value):
2✔
683
        value = self.to_python(value)
2✔
684
        if not isinstance(value, datetime.datetime):
2✔
685
            self.error("Only datetime objects may used in a ComplexDateTimeField")
2✔
686

687
    def to_python(self, value):
2✔
688
        original_value = value
2✔
689
        try:
2✔
690
            return self._convert_from_string(value)
2✔
691
        except Exception:
2✔
692
            return original_value
2✔
693

694
    def to_mongo(self, value):
2✔
695
        value = self.to_python(value)
2✔
696
        return self._convert_from_datetime(value)
2✔
697

698
    def prepare_query_value(self, op, value):
2✔
699
        if value is None:
2✔
700
            return value
2✔
701
        return super().prepare_query_value(op, self._convert_from_datetime(value))
2✔
702

703

704
class EmbeddedDocumentField(BaseField):
2✔
705
    """An embedded document field - with a declared document_type.
706
    Only valid values are subclasses of :class:`~mongoengine.EmbeddedDocument`.
707
    """
708

709
    def __init__(self, document_type, **kwargs):
2✔
710
        # XXX ValidationError raised outside of the "validate" method.
711
        if not (
2✔
712
            isinstance(document_type, str)
713
            or issubclass(document_type, EmbeddedDocument)
714
        ):
715
            self.error(
2✔
716
                "Invalid embedded document class provided to an "
717
                "EmbeddedDocumentField"
718
            )
719

720
        self.document_type_obj = document_type
2✔
721
        super().__init__(**kwargs)
2✔
722

723
    @property
2✔
724
    def document_type(self):
725
        if isinstance(self.document_type_obj, str):
2✔
726
            if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
2✔
727
                resolved_document_type = self.owner_document
2✔
728
            else:
729
                resolved_document_type = get_document(self.document_type_obj)
2✔
730

731
            if not issubclass(resolved_document_type, EmbeddedDocument):
2✔
732
                # Due to the late resolution of the document_type
733
                # There is a chance that it won't be an EmbeddedDocument (#1661)
734
                self.error(
2✔
735
                    "Invalid embedded document class provided to an "
736
                    "EmbeddedDocumentField"
737
                )
738
            self.document_type_obj = resolved_document_type
2✔
739

740
        return self.document_type_obj
2✔
741

742
    def to_python(self, value):
2✔
743
        if not isinstance(value, self.document_type):
2✔
744
            return self.document_type._from_son(
2✔
745
                value, _auto_dereference=self._auto_dereference
746
            )
747
        return value
2✔
748

749
    def to_mongo(self, value, use_db_field=True, fields=None):
2✔
750
        if not isinstance(value, self.document_type):
2✔
751
            return value
2✔
752
        return self.document_type.to_mongo(value, use_db_field, fields)
2✔
753

754
    def validate(self, value, clean=True):
2✔
755
        """Make sure that the document instance is an instance of the
756
        EmbeddedDocument subclass provided when the document was defined.
757
        """
758
        # Using isinstance also works for subclasses of self.document
759
        if not isinstance(value, self.document_type):
2✔
760
            self.error(
2✔
761
                "Invalid embedded document instance provided to an "
762
                "EmbeddedDocumentField"
763
            )
764
        self.document_type.validate(value, clean)
2✔
765

766
    def lookup_member(self, member_name):
2✔
767
        doc_and_subclasses = [self.document_type] + self.document_type.__subclasses__()
2✔
768
        for doc_type in doc_and_subclasses:
2✔
769
            field = doc_type._fields.get(member_name)
2✔
770
            if field:
2✔
771
                return field
2✔
772

773
    def prepare_query_value(self, op, value):
2✔
774
        if value is not None and not isinstance(value, self.document_type):
2✔
775
            # Short circuit for special operators, returning them as is
776
            if isinstance(value, dict) and all(k.startswith("$") for k in value.keys()):
2✔
777
                return value
2✔
778
            try:
2✔
779
                value = self.document_type._from_son(value)
2✔
780
            except ValueError:
2✔
781
                raise InvalidQueryError(
2✔
782
                    "Querying the embedded document '%s' failed, due to an invalid query value"
783
                    % (self.document_type._class_name,)
784
                )
785
        super().prepare_query_value(op, value)
2✔
786
        return self.to_mongo(value)
2✔
787

788

789
class GenericEmbeddedDocumentField(BaseField):
2✔
790
    """A generic embedded document field - allows any
791
    :class:`~mongoengine.EmbeddedDocument` to be stored.
792

793
    Only valid values are subclasses of :class:`~mongoengine.EmbeddedDocument`.
794

795
    .. note ::
796
        You can use the choices param to limit the acceptable
797
        EmbeddedDocument types
798
    """
799

800
    def prepare_query_value(self, op, value):
2✔
801
        return super().prepare_query_value(op, self.to_mongo(value))
2✔
802

803
    def to_python(self, value):
2✔
804
        if isinstance(value, dict):
2✔
805
            doc_cls = get_document(value["_cls"])
2✔
806
            value = doc_cls._from_son(value)
2✔
807

808
        return value
2✔
809

810
    def validate(self, value, clean=True):
2✔
811
        if self.choices and isinstance(value, SON):
2✔
812
            for choice in self.choices:
2✔
813
                if value["_cls"] == choice._class_name:
2✔
814
                    return True
2✔
815

816
        if not isinstance(value, EmbeddedDocument):
2✔
817
            self.error(
×
818
                "Invalid embedded document instance provided to an "
819
                "GenericEmbeddedDocumentField"
820
            )
821

822
        value.validate(clean=clean)
2✔
823

824
    def lookup_member(self, member_name):
2✔
825
        document_choices = self.choices or []
2✔
826
        for document_choice in document_choices:
2✔
827
            doc_and_subclasses = [document_choice] + document_choice.__subclasses__()
2✔
828
            for doc_type in doc_and_subclasses:
2✔
829
                field = doc_type._fields.get(member_name)
2✔
830
                if field:
2✔
831
                    return field
2✔
832

833
    def to_mongo(self, document, use_db_field=True, fields=None):
2✔
834
        if document is None:
2✔
835
            return None
×
836
        data = document.to_mongo(use_db_field, fields)
2✔
837
        if "_cls" not in data:
2✔
838
            data["_cls"] = document._class_name
2✔
839
        return data
2✔
840

841

842
class DynamicField(BaseField):
2✔
843
    """A truly dynamic field type capable of handling different and varying
844
    types of data.
845

846
    Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data"""
847

848
    def to_mongo(self, value, use_db_field=True, fields=None):
2✔
849
        """Convert a Python type to a MongoDB compatible type."""
850

851
        if isinstance(value, str):
2✔
852
            return value
2✔
853

854
        if hasattr(value, "to_mongo"):
2✔
855
            cls = value.__class__
2✔
856
            val = value.to_mongo(use_db_field, fields)
2✔
857
            # If we its a document thats not inherited add _cls
858
            if isinstance(value, Document):
2✔
859
                val = {"_ref": value.to_dbref(), "_cls": cls.__name__}
2✔
860
            if isinstance(value, EmbeddedDocument):
2✔
861
                val["_cls"] = cls.__name__
2✔
862
            return val
2✔
863

864
        if not isinstance(value, (dict, list, tuple)):
2✔
865
            return value
2✔
866

867
        is_list = False
2✔
868
        if not hasattr(value, "items"):
2✔
869
            is_list = True
2✔
870
            value = {k: v for k, v in enumerate(value)}
2✔
871

872
        data = {}
2✔
873
        for k, v in value.items():
2✔
874
            data[k] = self.to_mongo(v, use_db_field, fields)
2✔
875

876
        value = data
2✔
877
        if is_list:  # Convert back to a list
2✔
878
            value = [v for k, v in sorted(data.items(), key=itemgetter(0))]
2✔
879
        return value
2✔
880

881
    def to_python(self, value):
2✔
882
        if isinstance(value, dict) and "_cls" in value:
2✔
883
            doc_cls = get_document(value["_cls"])
2✔
884
            if "_ref" in value:
2✔
885
                value = doc_cls._get_db().dereference(value["_ref"])
2✔
886
            return doc_cls._from_son(value)
2✔
887

888
        return super().to_python(value)
2✔
889

890
    def lookup_member(self, member_name):
2✔
891
        return member_name
×
892

893
    def prepare_query_value(self, op, value):
2✔
894
        if isinstance(value, str):
2✔
895
            return StringField().prepare_query_value(op, value)
2✔
896
        return super().prepare_query_value(op, self.to_mongo(value))
2✔
897

898
    def validate(self, value, clean=True):
2✔
899
        if hasattr(value, "validate"):
2✔
900
            value.validate(clean=clean)
2✔
901

902

903
class ListField(ComplexBaseField):
2✔
904
    """A list field that wraps a standard field, allowing multiple instances
905
    of the field to be used as a list in the database.
906

907
    If using with ReferenceFields see: :ref:`many-to-many-with-listfields`
908

909
    .. note::
910
        Required means it cannot be empty - as the default for ListFields is []
911
    """
912

913
    def __init__(self, field=None, max_length=None, **kwargs):
2✔
914
        self.max_length = max_length
2✔
915
        kwargs.setdefault("default", lambda: [])
2✔
916
        super().__init__(field=field, **kwargs)
2✔
917

918
    def __get__(self, instance, owner):
2✔
919
        if instance is None:
2✔
920
            # Document class being used rather than a document object
921
            return self
2✔
922
        value = instance._data.get(self.name)
2✔
923
        LazyReferenceField = _import_class("LazyReferenceField")
2✔
924
        GenericLazyReferenceField = _import_class("GenericLazyReferenceField")
2✔
925
        if (
2✔
926
            isinstance(self.field, (LazyReferenceField, GenericLazyReferenceField))
927
            and value
928
        ):
929
            instance._data[self.name] = [self.field.build_lazyref(x) for x in value]
2✔
930
        return super().__get__(instance, owner)
2✔
931

932
    def validate(self, value):
2✔
933
        """Make sure that a list of valid fields is being used."""
934
        if not isinstance(value, (list, tuple, BaseQuerySet)):
2✔
935
            self.error("Only lists and tuples may be used in a list field")
2✔
936

937
        # Validate that max_length is not exceeded.
938
        # NOTE It's still possible to bypass this enforcement by using $push.
939
        # However, if the document is reloaded after $push and then re-saved,
940
        # the validation error will be raised.
941
        if self.max_length is not None and len(value) > self.max_length:
2✔
942
            self.error("List is too long")
2✔
943

944
        super().validate(value)
2✔
945

946
    def prepare_query_value(self, op, value):
2✔
947
        # Validate that the `set` operator doesn't contain more items than `max_length`.
948
        if op == "set" and self.max_length is not None and len(value) > self.max_length:
2✔
949
            self.error("List is too long")
2✔
950

951
        if self.field:
2✔
952
            # If the value is iterable and it's not a string nor a
953
            # BaseDocument, call prepare_query_value for each of its items.
954
            if (
2✔
955
                op in ("set", "unset", None)
956
                and hasattr(value, "__iter__")
957
                and not isinstance(value, str)
958
                and not isinstance(value, BaseDocument)
959
            ):
960
                return [self.field.prepare_query_value(op, v) for v in value]
2✔
961

962
            return self.field.prepare_query_value(op, value)
2✔
963

964
        return super().prepare_query_value(op, value)
2✔
965

966

967
class EmbeddedDocumentListField(ListField):
2✔
968
    """A :class:`~mongoengine.ListField` designed specially to hold a list of
969
    embedded documents to provide additional query helpers.
970

971
    .. note::
972
        The only valid list values are subclasses of
973
        :class:`~mongoengine.EmbeddedDocument`.
974
    """
975

976
    def __init__(self, document_type, **kwargs):
2✔
977
        """
978
        :param document_type: The type of
979
         :class:`~mongoengine.EmbeddedDocument` the list will hold.
980
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.ListField`
981
        """
982
        super().__init__(field=EmbeddedDocumentField(document_type), **kwargs)
2✔
983

984

985
class SortedListField(ListField):
2✔
986
    """A ListField that sorts the contents of its list before writing to
987
    the database in order to ensure that a sorted list is always
988
    retrieved.
989

990
    .. warning::
991
        There is a potential race condition when handling lists.  If you set /
992
        save the whole list then other processes trying to save the whole list
993
        as well could overwrite changes.  The safest way to append to a list is
994
        to perform a push operation.
995
    """
996

997
    def __init__(self, field, **kwargs):
2✔
998
        self._ordering = kwargs.pop("ordering", None)
2✔
999
        self._order_reverse = kwargs.pop("reverse", False)
2✔
1000
        super().__init__(field, **kwargs)
2✔
1001

1002
    def to_mongo(self, value, use_db_field=True, fields=None):
2✔
1003
        value = super().to_mongo(value, use_db_field, fields)
2✔
1004
        if self._ordering is not None:
2✔
1005
            return sorted(
2✔
1006
                value, key=itemgetter(self._ordering), reverse=self._order_reverse
1007
            )
1008
        return sorted(value, reverse=self._order_reverse)
2✔
1009

1010

1011
def key_not_string(d):
2✔
1012
    """Helper function to recursively determine if any key in a
1013
    dictionary is not a string.
1014
    """
1015
    for k, v in d.items():
2✔
1016
        if not isinstance(k, str) or (isinstance(v, dict) and key_not_string(v)):
2✔
1017
            return True
2✔
1018

1019

1020
def key_starts_with_dollar(d):
2✔
1021
    """Helper function to recursively determine if any key in a
1022
    dictionary starts with a dollar
1023
    """
1024
    for k, v in d.items():
2✔
1025
        if (k.startswith("$")) or (isinstance(v, dict) and key_starts_with_dollar(v)):
2✔
1026
            return True
2✔
1027

1028

1029
class DictField(ComplexBaseField):
2✔
1030
    """A dictionary field that wraps a standard Python dictionary. This is
1031
    similar to an embedded document, but the structure is not defined.
1032

1033
    .. note::
1034
        Required means it cannot be empty - as the default for DictFields is {}
1035
    """
1036

1037
    def __init__(self, field=None, *args, **kwargs):
2✔
1038
        self._auto_dereference = False
2✔
1039

1040
        kwargs.setdefault("default", lambda: {})
2✔
1041
        super().__init__(*args, field=field, **kwargs)
2✔
1042

1043
    def validate(self, value):
2✔
1044
        """Make sure that a list of valid fields is being used."""
1045
        if not isinstance(value, dict):
2✔
1046
            self.error("Only dictionaries may be used in a DictField")
2✔
1047

1048
        if key_not_string(value):
2✔
1049
            msg = "Invalid dictionary key - documents must have only string keys"
2✔
1050
            self.error(msg)
2✔
1051

1052
        # Following condition applies to MongoDB >= 3.6
1053
        # older Mongo has stricter constraints but
1054
        # it will be rejected upon insertion anyway
1055
        # Having a validation that depends on the MongoDB version
1056
        # is not straightforward as the field isn't aware of the connected Mongo
1057
        if key_starts_with_dollar(value):
2✔
1058
            self.error(
2✔
1059
                'Invalid dictionary key name - keys may not startswith "$" characters'
1060
            )
1061
        super().validate(value)
2✔
1062

1063
    def lookup_member(self, member_name):
2✔
1064
        return DictField(db_field=member_name)
2✔
1065

1066
    def prepare_query_value(self, op, value):
2✔
1067
        match_operators = [*STRING_OPERATORS]
2✔
1068

1069
        if op in match_operators and isinstance(value, str):
2✔
1070
            return StringField().prepare_query_value(op, value)
2✔
1071

1072
        if hasattr(
2✔
1073
            self.field, "field"
1074
        ):  # Used for instance when using DictField(ListField(IntField()))
1075
            if op in ("set", "unset") and isinstance(value, dict):
2✔
1076
                return {
2✔
1077
                    k: self.field.prepare_query_value(op, v) for k, v in value.items()
1078
                }
1079
            return self.field.prepare_query_value(op, value)
2✔
1080

1081
        return super().prepare_query_value(op, value)
2✔
1082

1083

1084
class MapField(DictField):
2✔
1085
    """A field that maps a name to a specified field type. Similar to
1086
    a DictField, except the 'value' of each item must match the specified
1087
    field type.
1088
    """
1089

1090
    def __init__(self, field=None, *args, **kwargs):
2✔
1091
        # XXX ValidationError raised outside the "validate" method.
1092
        if not isinstance(field, BaseField):
2✔
1093
            self.error("Argument to MapField constructor must be a valid field")
2✔
1094
        super().__init__(field=field, *args, **kwargs)
2✔
1095

1096

1097
class ReferenceField(BaseField):
2✔
1098
    """A reference to a document that will be automatically dereferenced on
1099
    access (lazily).
1100

1101
    Note this means you will get a database I/O access everytime you access
1102
    this field. This is necessary because the field returns a :class:`~mongoengine.Document`
1103
    which precise type can depend of the value of the `_cls` field present in the
1104
    document in database.
1105
    In short, using this type of field can lead to poor performances (especially
1106
    if you access this field only to retrieve it `pk` field which is already
1107
    known before dereference). To solve this you should consider using the
1108
    :class:`~mongoengine.fields.LazyReferenceField`.
1109

1110
    Use the `reverse_delete_rule` to handle what should happen if the document
1111
    the field is referencing is deleted.  EmbeddedDocuments, DictFields and
1112
    MapFields does not support reverse_delete_rule and an `InvalidDocumentError`
1113
    will be raised if trying to set on one of these Document / Field types.
1114

1115
    The options are:
1116

1117
      * DO_NOTHING (0)  - don't do anything (default).
1118
      * NULLIFY    (1)  - Updates the reference to null.
1119
      * CASCADE    (2)  - Deletes the documents associated with the reference.
1120
      * DENY       (3)  - Prevent the deletion of the reference object.
1121
      * PULL       (4)  - Pull the reference from a :class:`~mongoengine.fields.ListField` of references
1122

1123
    Alternative syntax for registering delete rules (useful when implementing
1124
    bi-directional delete rules)
1125

1126
    .. code-block:: python
1127

1128
        class Org(Document):
1129
            owner = ReferenceField('User')
1130

1131
        class User(Document):
1132
            org = ReferenceField('Org', reverse_delete_rule=CASCADE)
1133

1134
        User.register_delete_rule(Org, 'owner', DENY)
1135
    """
1136

1137
    def __init__(
2✔
1138
        self, document_type, dbref=False, reverse_delete_rule=DO_NOTHING, **kwargs
1139
    ):
1140
        """Initialises the Reference Field.
1141

1142
        :param document_type: The type of Document that will be referenced
1143
        :param dbref:  Store the reference as :class:`~pymongo.dbref.DBRef`
1144
          or as the :class:`~pymongo.objectid.ObjectId`.
1145
        :param reverse_delete_rule: Determines what to do when the referring
1146
          object is deleted
1147
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField`
1148

1149
        .. note ::
1150
            A reference to an abstract document type is always stored as a
1151
            :class:`~pymongo.dbref.DBRef`, regardless of the value of `dbref`.
1152
        """
1153
        # XXX ValidationError raised outside of the "validate" method.
1154
        if not isinstance(document_type, str) and not issubclass(
2✔
1155
            document_type, Document
1156
        ):
1157
            self.error(
2✔
1158
                "Argument to ReferenceField constructor must be a "
1159
                "document class or a string"
1160
            )
1161

1162
        self.dbref = dbref
2✔
1163
        self.document_type_obj = document_type
2✔
1164
        self.reverse_delete_rule = reverse_delete_rule
2✔
1165
        super().__init__(**kwargs)
2✔
1166

1167
    @property
2✔
1168
    def document_type(self):
1169
        if isinstance(self.document_type_obj, str):
2✔
1170
            if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
2✔
1171
                self.document_type_obj = self.owner_document
2✔
1172
            else:
1173
                self.document_type_obj = get_document(self.document_type_obj)
2✔
1174
        return self.document_type_obj
2✔
1175

1176
    @staticmethod
2✔
1177
    def _lazy_load_ref(ref_cls, dbref):
1178
        dereferenced_son = ref_cls._get_db().dereference(dbref)
2✔
1179
        if dereferenced_son is None:
2✔
1180
            raise DoesNotExist(f"Trying to dereference unknown document {dbref}")
2✔
1181

1182
        return ref_cls._from_son(dereferenced_son)
2✔
1183

1184
    def __get__(self, instance, owner):
2✔
1185
        """Descriptor to allow lazy dereferencing."""
1186
        if instance is None:
2✔
1187
            # Document class being used rather than a document object
1188
            return self
2✔
1189

1190
        # Get value from document instance if available
1191
        ref_value = instance._data.get(self.name)
2✔
1192
        auto_dereference = instance._fields[self.name]._auto_dereference
2✔
1193
        # Dereference DBRefs
1194
        if auto_dereference and isinstance(ref_value, DBRef):
2✔
1195
            if hasattr(ref_value, "cls"):
2✔
1196
                # Dereference using the class type specified in the reference
1197
                cls = get_document(ref_value.cls)
2✔
1198
            else:
1199
                cls = self.document_type
2✔
1200

1201
            instance._data[self.name] = self._lazy_load_ref(cls, ref_value)
2✔
1202

1203
        return super().__get__(instance, owner)
2✔
1204

1205
    def to_mongo(self, document):
2✔
1206
        if isinstance(document, DBRef):
2✔
1207
            if not self.dbref:
2✔
1208
                return document.id
2✔
1209
            return document
2✔
1210

1211
        if isinstance(document, Document):
2✔
1212
            # We need the id from the saved object to create the DBRef
1213
            id_ = document.pk
2✔
1214

1215
            # XXX ValidationError raised outside of the "validate" method.
1216
            if id_ is None:
2✔
1217
                self.error(
×
1218
                    "You can only reference documents once they have"
1219
                    " been saved to the database"
1220
                )
1221

1222
            # Use the attributes from the document instance, so that they
1223
            # override the attributes of this field's document type
1224
            cls = document
2✔
1225
        else:
1226
            id_ = document
2✔
1227
            cls = self.document_type
2✔
1228

1229
        id_field_name = cls._meta["id_field"]
2✔
1230
        id_field = cls._fields[id_field_name]
2✔
1231

1232
        id_ = id_field.to_mongo(id_)
2✔
1233
        if self.document_type._meta.get("abstract"):
2✔
1234
            collection = cls._get_collection_name()
2✔
1235
            return DBRef(collection, id_, cls=cls._class_name)
2✔
1236
        elif self.dbref:
2✔
1237
            collection = cls._get_collection_name()
2✔
1238
            return DBRef(collection, id_)
2✔
1239

1240
        return id_
2✔
1241

1242
    def to_python(self, value):
2✔
1243
        """Convert a MongoDB-compatible type to a Python type."""
1244
        if not self.dbref and not isinstance(
2✔
1245
            value, (DBRef, Document, EmbeddedDocument)
1246
        ):
1247
            collection = self.document_type._get_collection_name()
2✔
1248
            value = DBRef(collection, self.document_type.id.to_python(value))
2✔
1249
        return value
2✔
1250

1251
    def prepare_query_value(self, op, value):
2✔
1252
        if value is None:
2✔
1253
            return None
2✔
1254
        super().prepare_query_value(op, value)
2✔
1255
        return self.to_mongo(value)
2✔
1256

1257
    def validate(self, value):
2✔
1258
        if not isinstance(value, (self.document_type, LazyReference, DBRef, ObjectId)):
2✔
1259
            self.error(
2✔
1260
                "A ReferenceField only accepts DBRef, LazyReference, ObjectId or documents"
1261
            )
1262

1263
        if isinstance(value, Document) and value.id is None:
2✔
1264
            self.error(
2✔
1265
                "You can only reference documents once they have been "
1266
                "saved to the database"
1267
            )
1268

1269
    def lookup_member(self, member_name):
2✔
1270
        return self.document_type._fields.get(member_name)
×
1271

1272

1273
class CachedReferenceField(BaseField):
2✔
1274
    """A referencefield with cache fields to purpose pseudo-joins"""
1275

1276
    def __init__(self, document_type, fields=None, auto_sync=True, **kwargs):
2✔
1277
        """Initialises the Cached Reference Field.
1278

1279
        :param document_type: The type of Document that will be referenced
1280
        :param fields:  A list of fields to be cached in document
1281
        :param auto_sync: if True documents are auto updated
1282
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField`
1283
        """
1284
        if fields is None:
2✔
1285
            fields = []
2✔
1286

1287
        # XXX ValidationError raised outside of the "validate" method.
1288
        if not isinstance(document_type, str) and not (
2✔
1289
            inspect.isclass(document_type) and issubclass(document_type, Document)
1290
        ):
1291
            self.error(
2✔
1292
                "Argument to CachedReferenceField constructor must be a"
1293
                " document class or a string"
1294
            )
1295

1296
        self.auto_sync = auto_sync
2✔
1297
        self.document_type_obj = document_type
2✔
1298
        self.fields = fields
2✔
1299
        super().__init__(**kwargs)
2✔
1300

1301
    def start_listener(self):
2✔
1302
        from mongoengine import signals
2✔
1303

1304
        signals.post_save.connect(self.on_document_pre_save, sender=self.document_type)
2✔
1305

1306
    def on_document_pre_save(self, sender, document, created, **kwargs):
2✔
1307
        if created:
2✔
1308
            return None
2✔
1309

1310
        update_kwargs = {
2✔
1311
            f"set__{self.name}__{key}": val
1312
            for key, val in document._delta()[0].items()
1313
            if key in self.fields
1314
        }
1315
        if update_kwargs:
2✔
1316
            filter_kwargs = {}
2✔
1317
            filter_kwargs[self.name] = document
2✔
1318

1319
            self.owner_document.objects(**filter_kwargs).update(**update_kwargs)
2✔
1320

1321
    def to_python(self, value):
2✔
1322
        if isinstance(value, dict):
2✔
1323
            collection = self.document_type._get_collection_name()
2✔
1324
            value = DBRef(collection, self.document_type.id.to_python(value["_id"]))
2✔
1325
            return self.document_type._from_son(
2✔
1326
                self.document_type._get_db().dereference(value)
1327
            )
1328

1329
        return value
2✔
1330

1331
    @property
2✔
1332
    def document_type(self):
1333
        if isinstance(self.document_type_obj, str):
2✔
1334
            if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
2✔
1335
                self.document_type_obj = self.owner_document
2✔
1336
            else:
1337
                self.document_type_obj = get_document(self.document_type_obj)
×
1338
        return self.document_type_obj
2✔
1339

1340
    @staticmethod
2✔
1341
    def _lazy_load_ref(ref_cls, dbref):
1342
        dereferenced_son = ref_cls._get_db().dereference(dbref)
×
1343
        if dereferenced_son is None:
×
1344
            raise DoesNotExist(f"Trying to dereference unknown document {dbref}")
×
1345

1346
        return ref_cls._from_son(dereferenced_son)
×
1347

1348
    def __get__(self, instance, owner):
2✔
1349
        if instance is None:
2✔
1350
            # Document class being used rather than a document object
1351
            return self
2✔
1352

1353
        # Get value from document instance if available
1354
        value = instance._data.get(self.name)
2✔
1355
        auto_dereference = instance._fields[self.name]._auto_dereference
2✔
1356

1357
        # Dereference DBRefs
1358
        if auto_dereference and isinstance(value, DBRef):
2✔
1359
            instance._data[self.name] = self._lazy_load_ref(self.document_type, value)
×
1360

1361
        return super().__get__(instance, owner)
2✔
1362

1363
    def to_mongo(self, document, use_db_field=True, fields=None):
2✔
1364
        id_field_name = self.document_type._meta["id_field"]
2✔
1365
        id_field = self.document_type._fields[id_field_name]
2✔
1366

1367
        # XXX ValidationError raised outside of the "validate" method.
1368
        if isinstance(document, Document):
2✔
1369
            # We need the id from the saved object to create the DBRef
1370
            id_ = document.pk
2✔
1371
            if id_ is None:
2✔
1372
                self.error(
×
1373
                    "You can only reference documents once they have"
1374
                    " been saved to the database"
1375
                )
1376
        else:
1377
            self.error("Only accept a document object")
×
1378

1379
        value = SON((("_id", id_field.to_mongo(id_)),))
2✔
1380

1381
        if fields:
2✔
1382
            new_fields = [f for f in self.fields if f in fields]
×
1383
        else:
1384
            new_fields = self.fields
2✔
1385

1386
        value.update(dict(document.to_mongo(use_db_field, fields=new_fields)))
2✔
1387
        return value
2✔
1388

1389
    def prepare_query_value(self, op, value):
2✔
1390
        if value is None:
2✔
1391
            return None
2✔
1392

1393
        # XXX ValidationError raised outside of the "validate" method.
1394
        if isinstance(value, Document):
2✔
1395
            if value.pk is None:
2✔
1396
                self.error(
×
1397
                    "You can only reference documents once they have"
1398
                    " been saved to the database"
1399
                )
1400
            value_dict = {"_id": value.pk}
2✔
1401
            for field in self.fields:
2✔
1402
                value_dict.update({field: value[field]})
2✔
1403

1404
            return value_dict
2✔
1405

1406
        raise NotImplementedError
×
1407

1408
    def validate(self, value):
2✔
1409
        if not isinstance(value, self.document_type):
2✔
1410
            self.error("A CachedReferenceField only accepts documents")
×
1411

1412
        if isinstance(value, Document) and value.id is None:
2✔
1413
            self.error(
×
1414
                "You can only reference documents once they have been "
1415
                "saved to the database"
1416
            )
1417

1418
    def lookup_member(self, member_name):
2✔
1419
        return self.document_type._fields.get(member_name)
2✔
1420

1421
    def sync_all(self):
2✔
1422
        """
1423
        Sync all cached fields on demand.
1424
        Caution: this operation may be slower.
1425
        """
1426
        update_key = "set__%s" % self.name
2✔
1427

1428
        for doc in self.document_type.objects:
2✔
1429
            filter_kwargs = {}
2✔
1430
            filter_kwargs[self.name] = doc
2✔
1431

1432
            update_kwargs = {}
2✔
1433
            update_kwargs[update_key] = doc
2✔
1434

1435
            self.owner_document.objects(**filter_kwargs).update(**update_kwargs)
2✔
1436

1437

1438
class GenericReferenceField(BaseField):
2✔
1439
    """A reference to *any* :class:`~mongoengine.document.Document` subclass
1440
    that will be automatically dereferenced on access (lazily).
1441

1442
    Note this field works the same way as :class:`~mongoengine.document.ReferenceField`,
1443
    doing database I/O access the first time it is accessed (even if it's to access
1444
    it ``pk`` or ``id`` field).
1445
    To solve this you should consider using the
1446
    :class:`~mongoengine.fields.GenericLazyReferenceField`.
1447

1448
    .. note ::
1449
        * Any documents used as a generic reference must be registered in the
1450
          document registry.  Importing the model will automatically register
1451
          it.
1452

1453
        * You can use the choices param to limit the acceptable Document types
1454
    """
1455

1456
    def __init__(self, *args, **kwargs):
2✔
1457
        choices = kwargs.pop("choices", None)
2✔
1458
        super().__init__(*args, **kwargs)
2✔
1459
        self.choices = []
2✔
1460
        # Keep the choices as a list of allowed Document class names
1461
        if choices:
2✔
1462
            for choice in choices:
2✔
1463
                if isinstance(choice, str):
2✔
1464
                    self.choices.append(choice)
2✔
1465
                elif isinstance(choice, type) and issubclass(choice, Document):
2✔
1466
                    self.choices.append(choice._class_name)
2✔
1467
                else:
1468
                    # XXX ValidationError raised outside of the "validate"
1469
                    # method.
1470
                    self.error(
×
1471
                        "Invalid choices provided: must be a list of"
1472
                        "Document subclasses and/or str"
1473
                    )
1474

1475
    def _validate_choices(self, value):
2✔
1476
        if isinstance(value, dict):
2✔
1477
            # If the field has not been dereferenced, it is still a dict
1478
            # of class and DBRef
1479
            value = value.get("_cls")
2✔
1480
        elif isinstance(value, Document):
2✔
1481
            value = value._class_name
2✔
1482
        super()._validate_choices(value)
2✔
1483

1484
    @staticmethod
2✔
1485
    def _lazy_load_ref(ref_cls, dbref):
1486
        dereferenced_son = ref_cls._get_db().dereference(dbref)
2✔
1487
        if dereferenced_son is None:
2✔
1488
            raise DoesNotExist(f"Trying to dereference unknown document {dbref}")
2✔
1489

1490
        return ref_cls._from_son(dereferenced_son)
2✔
1491

1492
    def __get__(self, instance, owner):
2✔
1493
        if instance is None:
2✔
1494
            return self
×
1495

1496
        value = instance._data.get(self.name)
2✔
1497

1498
        auto_dereference = instance._fields[self.name]._auto_dereference
2✔
1499
        if auto_dereference and isinstance(value, dict):
2✔
1500
            doc_cls = get_document(value["_cls"])
2✔
1501
            instance._data[self.name] = self._lazy_load_ref(doc_cls, value["_ref"])
2✔
1502

1503
        return super().__get__(instance, owner)
2✔
1504

1505
    def validate(self, value):
2✔
1506
        if not isinstance(value, (Document, DBRef, dict, SON)):
2✔
1507
            self.error("GenericReferences can only contain documents")
2✔
1508

1509
        if isinstance(value, (dict, SON)):
2✔
1510
            if "_ref" not in value or "_cls" not in value:
2✔
1511
                self.error("GenericReferences can only contain documents")
×
1512

1513
        # We need the id from the saved object to create the DBRef
1514
        elif isinstance(value, Document) and value.id is None:
2✔
1515
            self.error(
2✔
1516
                "You can only reference documents once they have been"
1517
                " saved to the database"
1518
            )
1519

1520
    def to_mongo(self, document):
2✔
1521
        if document is None:
2✔
1522
            return None
×
1523

1524
        if isinstance(document, (dict, SON, ObjectId, DBRef)):
2✔
1525
            return document
2✔
1526

1527
        id_field_name = document.__class__._meta["id_field"]
2✔
1528
        id_field = document.__class__._fields[id_field_name]
2✔
1529

1530
        if isinstance(document, Document):
2✔
1531
            # We need the id from the saved object to create the DBRef
1532
            id_ = document.id
2✔
1533
            if id_ is None:
2✔
1534
                # XXX ValidationError raised outside of the "validate" method.
1535
                self.error(
×
1536
                    "You can only reference documents once they have"
1537
                    " been saved to the database"
1538
                )
1539
        else:
1540
            id_ = document
×
1541

1542
        id_ = id_field.to_mongo(id_)
2✔
1543
        collection = document._get_collection_name()
2✔
1544
        ref = DBRef(collection, id_)
2✔
1545
        return SON((("_cls", document._class_name), ("_ref", ref)))
2✔
1546

1547
    def prepare_query_value(self, op, value):
2✔
1548
        if value is None:
2✔
1549
            return None
2✔
1550

1551
        return self.to_mongo(value)
2✔
1552

1553

1554
class BinaryField(BaseField):
2✔
1555
    """A binary data field."""
1556

1557
    def __init__(self, max_bytes=None, **kwargs):
2✔
1558
        self.max_bytes = max_bytes
2✔
1559
        super().__init__(**kwargs)
2✔
1560

1561
    def __set__(self, instance, value):
2✔
1562
        """Handle bytearrays in python 3.1"""
1563
        if isinstance(value, bytearray):
2✔
1564
            value = bytes(value)
2✔
1565
        return super().__set__(instance, value)
2✔
1566

1567
    def to_mongo(self, value):
2✔
1568
        return Binary(value)
2✔
1569

1570
    def validate(self, value):
2✔
1571
        if not isinstance(value, (bytes, Binary)):
2✔
1572
            self.error(
2✔
1573
                "BinaryField only accepts instances of "
1574
                "(%s, %s, Binary)" % (bytes.__name__, Binary.__name__)
1575
            )
1576

1577
        if self.max_bytes is not None and len(value) > self.max_bytes:
2✔
1578
            self.error("Binary value is too long")
2✔
1579

1580
    def prepare_query_value(self, op, value):
2✔
1581
        if value is None:
2✔
1582
            return value
×
1583
        return super().prepare_query_value(op, self.to_mongo(value))
2✔
1584

1585

1586
class EnumField(BaseField):
2✔
1587
    """Enumeration Field. Values are stored underneath as is,
1588
    so it will only work with simple types (str, int, etc) that
1589
    are bson encodable
1590

1591
    Example usage:
1592

1593
    .. code-block:: python
1594

1595
        class Status(Enum):
1596
            NEW = 'new'
1597
            ONGOING = 'ongoing'
1598
            DONE = 'done'
1599

1600
        class ModelWithEnum(Document):
1601
            status = EnumField(Status, default=Status.NEW)
1602

1603
        ModelWithEnum(status='done')
1604
        ModelWithEnum(status=Status.DONE)
1605

1606
    Enum fields can be searched using enum or its value:
1607

1608
    .. code-block:: python
1609

1610
        ModelWithEnum.objects(status='new').count()
1611
        ModelWithEnum.objects(status=Status.NEW).count()
1612

1613
    The values can be restricted to a subset of the enum by using the ``choices`` parameter:
1614

1615
    .. code-block:: python
1616

1617
        class ModelWithEnum(Document):
1618
            status = EnumField(Status, choices=[Status.NEW, Status.DONE])
1619
    """
1620

1621
    def __init__(self, enum, **kwargs):
2✔
1622
        self._enum_cls = enum
2✔
1623
        if kwargs.get("choices"):
2✔
1624
            invalid_choices = []
2✔
1625
            for choice in kwargs["choices"]:
2✔
1626
                if not isinstance(choice, enum):
2✔
1627
                    invalid_choices.append(choice)
2✔
1628
            if invalid_choices:
2✔
1629
                raise ValueError("Invalid choices: %r" % invalid_choices)
2✔
1630
        else:
1631
            kwargs["choices"] = list(self._enum_cls)  # Implicit validator
2✔
1632
        super().__init__(**kwargs)
2✔
1633

1634
    def validate(self, value):
2✔
1635
        if isinstance(value, self._enum_cls):
2✔
1636
            return super().validate(value)
2✔
1637
        try:
×
1638
            self._enum_cls(value)
×
1639
        except ValueError:
×
1640
            self.error(f"{value} is not a valid {self._enum_cls}")
×
1641

1642
    def to_python(self, value):
2✔
1643
        value = super().to_python(value)
2✔
1644
        if not isinstance(value, self._enum_cls):
2✔
1645
            try:
2✔
1646
                return self._enum_cls(value)
2✔
1647
            except ValueError:
2✔
1648
                return value
2✔
1649
        return value
2✔
1650

1651
    def __set__(self, instance, value):
2✔
1652
        return super().__set__(instance, self.to_python(value))
2✔
1653

1654
    def to_mongo(self, value):
2✔
1655
        if isinstance(value, self._enum_cls):
2✔
1656
            return value.value
2✔
1657
        return value
2✔
1658

1659
    def prepare_query_value(self, op, value):
2✔
1660
        if value is None:
2✔
1661
            return value
2✔
1662
        return super().prepare_query_value(op, self.to_mongo(value))
2✔
1663

1664

1665
class GridFSError(Exception):
2✔
1666
    pass
2✔
1667

1668

1669
class GridFSProxy:
2✔
1670
    """Proxy object to handle writing and reading of files to and from GridFS"""
1671

1672
    _fs = None
2✔
1673

1674
    def __init__(
2✔
1675
        self,
1676
        grid_id=None,
1677
        key=None,
1678
        instance=None,
1679
        db_alias=DEFAULT_CONNECTION_NAME,
1680
        collection_name="fs",
1681
    ):
1682
        self.grid_id = grid_id  # Store GridFS id for file
2✔
1683
        self.key = key
2✔
1684
        self.instance = instance
2✔
1685
        self.db_alias = db_alias
2✔
1686
        self.collection_name = collection_name
2✔
1687
        self.newfile = None  # Used for partial writes
2✔
1688
        self.gridout = None
2✔
1689

1690
    def __getattr__(self, name):
2✔
1691
        attrs = (
2✔
1692
            "_fs",
1693
            "grid_id",
1694
            "key",
1695
            "instance",
1696
            "db_alias",
1697
            "collection_name",
1698
            "newfile",
1699
            "gridout",
1700
        )
1701
        if name in attrs:
2✔
1702
            return self.__getattribute__(name)
×
1703
        obj = self.get()
2✔
1704
        if hasattr(obj, name):
2✔
1705
            return getattr(obj, name)
2✔
1706
        raise AttributeError
2✔
1707

1708
    def __get__(self, instance, value):
2✔
1709
        return self
×
1710

1711
    def __bool__(self):
2✔
1712
        return bool(self.grid_id)
2✔
1713

1714
    def __getstate__(self):
2✔
1715
        self_dict = self.__dict__
×
1716
        self_dict["_fs"] = None
×
1717
        return self_dict
×
1718

1719
    def __copy__(self):
2✔
1720
        copied = GridFSProxy()
×
1721
        copied.__dict__.update(self.__getstate__())
×
1722
        return copied
×
1723

1724
    def __deepcopy__(self, memo):
2✔
1725
        return self.__copy__()
×
1726

1727
    def __repr__(self):
2✔
1728
        return f"<{self.__class__.__name__}: {self.grid_id}>"
×
1729

1730
    def __str__(self):
2✔
1731
        gridout = self.get()
2✔
1732
        filename = gridout.filename if gridout else "<no file>"
2✔
1733
        return f"<{self.__class__.__name__}: {filename} ({self.grid_id})>"
2✔
1734

1735
    def __eq__(self, other):
2✔
1736
        if isinstance(other, GridFSProxy):
2✔
1737
            return (
2✔
1738
                (self.grid_id == other.grid_id)
1739
                and (self.collection_name == other.collection_name)
1740
                and (self.db_alias == other.db_alias)
1741
            )
1742
        else:
1743
            return False
2✔
1744

1745
    def __ne__(self, other):
2✔
1746
        return not self == other
×
1747

1748
    @property
2✔
1749
    def fs(self):
1750
        if not self._fs:
2✔
1751
            self._fs = gridfs.GridFS(get_db(self.db_alias), self.collection_name)
2✔
1752
        return self._fs
2✔
1753

1754
    def get(self, grid_id=None):
2✔
1755
        if grid_id:
2✔
1756
            self.grid_id = grid_id
×
1757

1758
        if self.grid_id is None:
2✔
1759
            return None
2✔
1760

1761
        try:
2✔
1762
            if self.gridout is None:
2✔
1763
                self.gridout = self.fs.get(self.grid_id)
2✔
1764
            return self.gridout
2✔
1765
        except Exception:
×
1766
            # File has been deleted
1767
            return None
×
1768

1769
    def new_file(self, **kwargs):
2✔
1770
        self.newfile = self.fs.new_file(**kwargs)
2✔
1771
        self.grid_id = self.newfile._id
2✔
1772
        self._mark_as_changed()
2✔
1773

1774
    def put(self, file_obj, **kwargs):
2✔
1775
        if self.grid_id:
2✔
1776
            raise GridFSError(
×
1777
                "This document already has a file. Either delete "
1778
                "it or call replace to overwrite it"
1779
            )
1780
        self.grid_id = self.fs.put(file_obj, **kwargs)
2✔
1781
        self._mark_as_changed()
2✔
1782

1783
    def write(self, string):
2✔
1784
        if self.grid_id:
2✔
1785
            if not self.newfile:
2✔
1786
                raise GridFSError(
×
1787
                    "This document already has a file. Either "
1788
                    "delete it or call replace to overwrite it"
1789
                )
1790
        else:
1791
            self.new_file()
×
1792
        self.newfile.write(string)
2✔
1793

1794
    def writelines(self, lines):
2✔
1795
        if not self.newfile:
×
1796
            self.new_file()
×
1797
            self.grid_id = self.newfile._id
×
1798
        self.newfile.writelines(lines)
×
1799

1800
    def read(self, size=-1):
2✔
1801
        gridout = self.get()
2✔
1802
        if gridout is None:
2✔
1803
            return None
2✔
1804
        else:
1805
            try:
2✔
1806
                return gridout.read(size)
2✔
1807
            except Exception:
×
1808
                return ""
×
1809

1810
    def delete(self):
2✔
1811
        # Delete file from GridFS, FileField still remains
1812
        self.fs.delete(self.grid_id)
2✔
1813
        self.grid_id = None
2✔
1814
        self.gridout = None
2✔
1815
        self._mark_as_changed()
2✔
1816

1817
    def replace(self, file_obj, **kwargs):
2✔
1818
        self.delete()
2✔
1819
        self.put(file_obj, **kwargs)
2✔
1820

1821
    def close(self):
2✔
1822
        if self.newfile:
2✔
1823
            self.newfile.close()
2✔
1824

1825
    def _mark_as_changed(self):
2✔
1826
        """Inform the instance that `self.key` has been changed"""
1827
        if self.instance:
2✔
1828
            self.instance._mark_as_changed(self.key)
2✔
1829

1830

1831
class FileField(BaseField):
2✔
1832
    """A GridFS storage field."""
1833

1834
    proxy_class = GridFSProxy
2✔
1835

1836
    def __init__(
2✔
1837
        self, db_alias=DEFAULT_CONNECTION_NAME, collection_name="fs", **kwargs
1838
    ):
1839
        super().__init__(**kwargs)
2✔
1840
        self.collection_name = collection_name
2✔
1841
        self.db_alias = db_alias
2✔
1842

1843
    def __get__(self, instance, owner):
2✔
1844
        if instance is None:
2✔
1845
            return self
×
1846

1847
        # Check if a file already exists for this model
1848
        grid_file = instance._data.get(self.name)
2✔
1849
        if not isinstance(grid_file, self.proxy_class):
2✔
1850
            grid_file = self.get_proxy_obj(key=self.name, instance=instance)
2✔
1851
            instance._data[self.name] = grid_file
2✔
1852

1853
        if not grid_file.key:
2✔
1854
            grid_file.key = self.name
2✔
1855
            grid_file.instance = instance
2✔
1856
        return grid_file
2✔
1857

1858
    def __set__(self, instance, value):
2✔
1859
        key = self.name
2✔
1860
        if (
2✔
1861
            hasattr(value, "read") and not isinstance(value, GridFSProxy)
1862
        ) or isinstance(value, (bytes, str)):
1863
            # using "FileField() = file/string" notation
1864
            grid_file = instance._data.get(self.name)
2✔
1865
            # If a file already exists, delete it
1866
            if grid_file:
2✔
1867
                try:
2✔
1868
                    grid_file.delete()
2✔
1869
                except Exception:
×
1870
                    pass
×
1871

1872
            # Create a new proxy object as we don't already have one
1873
            instance._data[key] = self.get_proxy_obj(key=key, instance=instance)
2✔
1874
            instance._data[key].put(value)
2✔
1875
        else:
1876
            instance._data[key] = value
2✔
1877

1878
        instance._mark_as_changed(key)
2✔
1879

1880
    def get_proxy_obj(self, key, instance, db_alias=None, collection_name=None):
2✔
1881
        if db_alias is None:
2✔
1882
            db_alias = self.db_alias
2✔
1883
        if collection_name is None:
2✔
1884
            collection_name = self.collection_name
2✔
1885

1886
        return self.proxy_class(
2✔
1887
            key=key,
1888
            instance=instance,
1889
            db_alias=db_alias,
1890
            collection_name=collection_name,
1891
        )
1892

1893
    def to_mongo(self, value):
2✔
1894
        # Store the GridFS file id in MongoDB
1895
        if isinstance(value, self.proxy_class) and value.grid_id is not None:
2✔
1896
            return value.grid_id
2✔
1897
        return None
2✔
1898

1899
    def to_python(self, value):
2✔
1900
        if value is not None:
2✔
1901
            return self.proxy_class(
2✔
1902
                value, collection_name=self.collection_name, db_alias=self.db_alias
1903
            )
1904

1905
    def validate(self, value):
2✔
1906
        if value.grid_id is not None:
2✔
1907
            if not isinstance(value, self.proxy_class):
2✔
1908
                self.error("FileField only accepts GridFSProxy values")
×
1909
            if not isinstance(value.grid_id, ObjectId):
2✔
1910
                self.error("Invalid GridFSProxy value")
×
1911

1912

1913
class ImageGridFsProxy(GridFSProxy):
2✔
1914
    """Proxy for ImageField"""
1915

1916
    def put(self, file_obj, **kwargs):
2✔
1917
        """
1918
        Insert a image in database
1919
        applying field properties (size, thumbnail_size)
1920
        """
1921
        field = self.instance._fields[self.key]
2✔
1922
        # Handle nested fields
1923
        if hasattr(field, "field") and isinstance(field.field, FileField):
2✔
1924
            field = field.field
×
1925

1926
        try:
2✔
1927
            img = Image.open(file_obj)
2✔
1928
            img_format = img.format
2✔
1929
        except Exception as e:
2✔
1930
            raise ValidationError("Invalid image: %s" % e)
2✔
1931

1932
        # Progressive JPEG
1933
        # TODO: fixme, at least unused, at worst bad implementation
1934
        progressive = img.info.get("progressive") or False
2✔
1935

1936
        if (
2✔
1937
            kwargs.get("progressive")
1938
            and isinstance(kwargs.get("progressive"), bool)
1939
            and img_format == "JPEG"
1940
        ):
1941
            progressive = True
×
1942
        else:
1943
            progressive = False
2✔
1944

1945
        if field.size and (
2✔
1946
            img.size[0] > field.size["width"] or img.size[1] > field.size["height"]
1947
        ):
1948
            size = field.size
2✔
1949

1950
            if size["force"]:
2✔
1951
                img = ImageOps.fit(img, (size["width"], size["height"]), LANCZOS)
2✔
1952
            else:
1953
                img.thumbnail((size["width"], size["height"]), LANCZOS)
×
1954

1955
        thumbnail = None
2✔
1956
        if field.thumbnail_size:
2✔
1957
            size = field.thumbnail_size
2✔
1958

1959
            if size["force"]:
2✔
1960
                thumbnail = ImageOps.fit(img, (size["width"], size["height"]), LANCZOS)
2✔
1961
            else:
1962
                thumbnail = img.copy()
×
1963
                thumbnail.thumbnail((size["width"], size["height"]), LANCZOS)
×
1964

1965
        if thumbnail:
2✔
1966
            thumb_id = self._put_thumbnail(thumbnail, img_format, progressive)
2✔
1967
        else:
1968
            thumb_id = None
2✔
1969

1970
        w, h = img.size
2✔
1971

1972
        io = BytesIO()
2✔
1973
        img.save(io, img_format, progressive=progressive)
2✔
1974
        io.seek(0)
2✔
1975

1976
        return super().put(
2✔
1977
            io, width=w, height=h, format=img_format, thumbnail_id=thumb_id, **kwargs
1978
        )
1979

1980
    def delete(self, *args, **kwargs):
2✔
1981
        # deletes thumbnail
1982
        out = self.get()
2✔
1983
        if out and out.thumbnail_id:
2✔
1984
            self.fs.delete(out.thumbnail_id)
2✔
1985

1986
        return super().delete()
2✔
1987

1988
    def _put_thumbnail(self, thumbnail, format, progressive, **kwargs):
2✔
1989
        w, h = thumbnail.size
2✔
1990

1991
        io = BytesIO()
2✔
1992
        thumbnail.save(io, format, progressive=progressive)
2✔
1993
        io.seek(0)
2✔
1994

1995
        return self.fs.put(io, width=w, height=h, format=format, **kwargs)
2✔
1996

1997
    @property
2✔
1998
    def size(self):
1999
        """
2000
        return a width, height of image
2001
        """
2002
        out = self.get()
2✔
2003
        if out:
2✔
2004
            return out.width, out.height
2✔
2005

2006
    @property
2✔
2007
    def format(self):
2008
        """
2009
        return format of image
2010
        ex: PNG, JPEG, GIF, etc
2011
        """
2012
        out = self.get()
2✔
2013
        if out:
2✔
2014
            return out.format
2✔
2015

2016
    @property
2✔
2017
    def thumbnail(self):
2018
        """
2019
        return a gridfs.grid_file.GridOut
2020
        representing a thumbnail of Image
2021
        """
2022
        out = self.get()
2✔
2023
        if out and out.thumbnail_id:
2✔
2024
            return self.fs.get(out.thumbnail_id)
2✔
2025

2026
    def write(self, *args, **kwargs):
2✔
2027
        raise RuntimeError('Please use "put" method instead')
×
2028

2029
    def writelines(self, *args, **kwargs):
2✔
2030
        raise RuntimeError('Please use "put" method instead')
×
2031

2032

2033
class ImproperlyConfigured(Exception):
2✔
2034
    pass
2✔
2035

2036

2037
class ImageField(FileField):
2✔
2038
    """
2039
    A Image File storage field.
2040

2041
    :param size: max size to store images, provided as (width, height, force)
2042
        if larger, it will be automatically resized (ex: size=(800, 600, True))
2043
    :param thumbnail_size: size to generate a thumbnail, provided as (width, height, force)
2044
    """
2045

2046
    proxy_class = ImageGridFsProxy
2✔
2047

2048
    def __init__(
2✔
2049
        self, size=None, thumbnail_size=None, collection_name="images", **kwargs
2050
    ):
2051
        if not Image:
2✔
2052
            raise ImproperlyConfigured("PIL library was not found")
×
2053

2054
        params_size = ("width", "height", "force")
2✔
2055
        extra_args = {"size": size, "thumbnail_size": thumbnail_size}
2✔
2056
        for att_name, att in extra_args.items():
2✔
2057
            value = None
2✔
2058
            if isinstance(att, (tuple, list)):
2✔
2059
                value = dict(itertools.zip_longest(params_size, att, fillvalue=None))
2✔
2060

2061
            setattr(self, att_name, value)
2✔
2062

2063
        super().__init__(collection_name=collection_name, **kwargs)
2✔
2064

2065

2066
class SequenceField(BaseField):
2✔
2067
    """Provides a sequential counter see:
2068
     https://docs.mongodb.com/manual/reference/method/ObjectId/#ObjectIDs-SequenceNumbers
2069

2070
    .. note::
2071

2072
             Although traditional databases often use increasing sequence
2073
             numbers for primary keys. In MongoDB, the preferred approach is to
2074
             use Object IDs instead.  The concept is that in a very large
2075
             cluster of machines, it is easier to create an object ID than have
2076
             global, uniformly increasing sequence numbers.
2077

2078
    :param collection_name:  Name of the counter collection (default 'mongoengine.counters')
2079
    :param sequence_name: Name of the sequence in the collection (default 'ClassName.counter')
2080
    :param value_decorator: Any callable to use as a counter (default int)
2081

2082
    Use any callable as `value_decorator` to transform calculated counter into
2083
    any value suitable for your needs, e.g. string or hexadecimal
2084
    representation of the default integer counter value.
2085

2086
    .. note::
2087

2088
        In case the counter is defined in the abstract document, it will be
2089
        common to all inherited documents and the default sequence name will
2090
        be the class name of the abstract document.
2091
    """
2092

2093
    _auto_gen = True
2✔
2094
    COLLECTION_NAME = "mongoengine.counters"
2✔
2095
    VALUE_DECORATOR = int
2✔
2096

2097
    def __init__(
2✔
2098
        self,
2099
        collection_name=None,
2100
        db_alias=None,
2101
        sequence_name=None,
2102
        value_decorator=None,
2103
        *args,
2104
        **kwargs,
2105
    ):
2106
        self.collection_name = collection_name or self.COLLECTION_NAME
2✔
2107
        self.db_alias = db_alias or DEFAULT_CONNECTION_NAME
2✔
2108
        self.sequence_name = sequence_name
2✔
2109
        self.value_decorator = (
2✔
2110
            value_decorator if callable(value_decorator) else self.VALUE_DECORATOR
2111
        )
2112
        super().__init__(*args, **kwargs)
2✔
2113

2114
    def generate(self):
2✔
2115
        """
2116
        Generate and Increment the counter
2117
        """
2118
        sequence_name = self.get_sequence_name()
2✔
2119
        sequence_id = f"{sequence_name}.{self.name}"
2✔
2120
        collection = get_db(alias=self.db_alias)[self.collection_name]
2✔
2121

2122
        counter = collection.find_one_and_update(
2✔
2123
            filter={"_id": sequence_id},
2124
            update={"$inc": {"next": 1}},
2125
            return_document=ReturnDocument.AFTER,
2126
            upsert=True,
2127
        )
2128
        return self.value_decorator(counter["next"])
2✔
2129

2130
    def set_next_value(self, value):
2✔
2131
        """Helper method to set the next sequence value"""
2132
        sequence_name = self.get_sequence_name()
2✔
2133
        sequence_id = f"{sequence_name}.{self.name}"
2✔
2134
        collection = get_db(alias=self.db_alias)[self.collection_name]
2✔
2135
        counter = collection.find_one_and_update(
2✔
2136
            filter={"_id": sequence_id},
2137
            update={"$set": {"next": value}},
2138
            return_document=ReturnDocument.AFTER,
2139
            upsert=True,
2140
        )
2141
        return self.value_decorator(counter["next"])
2✔
2142

2143
    def get_next_value(self):
2✔
2144
        """Helper method to get the next value for previewing.
2145

2146
        .. warning:: There is no guarantee this will be the next value
2147
        as it is only fixed on set.
2148
        """
2149
        sequence_name = self.get_sequence_name()
2✔
2150
        sequence_id = f"{sequence_name}.{self.name}"
2✔
2151
        collection = get_db(alias=self.db_alias)[self.collection_name]
2✔
2152
        data = collection.find_one({"_id": sequence_id})
2✔
2153

2154
        if data:
2✔
2155
            return self.value_decorator(data["next"] + 1)
2✔
2156

2157
        return self.value_decorator(1)
2✔
2158

2159
    def get_sequence_name(self):
2✔
2160
        if self.sequence_name:
2✔
2161
            return self.sequence_name
2✔
2162
        owner = self.owner_document
2✔
2163
        if issubclass(owner, Document) and not owner._meta.get("abstract"):
2✔
2164
            return owner._get_collection_name()
2✔
2165
        else:
2166
            return (
2✔
2167
                "".join("_%s" % c if c.isupper() else c for c in owner._class_name)
2168
                .strip("_")
2169
                .lower()
2170
            )
2171

2172
    def __get__(self, instance, owner):
2✔
2173
        value = super().__get__(instance, owner)
2✔
2174
        if value is None and instance._initialised:
2✔
2175
            value = self.generate()
2✔
2176
            instance._data[self.name] = value
2✔
2177
            instance._mark_as_changed(self.name)
2✔
2178

2179
        return value
2✔
2180

2181
    def __set__(self, instance, value):
2✔
2182
        if value is None and instance._initialised:
2✔
2183
            value = self.generate()
2✔
2184

2185
        return super().__set__(instance, value)
2✔
2186

2187
    def prepare_query_value(self, op, value):
2✔
2188
        """
2189
        This method is overridden in order to convert the query value into to required
2190
        type. We need to do this in order to be able to successfully compare query
2191
        values passed as string, the base implementation returns the value as is.
2192
        """
2193
        return self.value_decorator(value)
2✔
2194

2195
    def to_python(self, value):
2✔
2196
        if value is None:
2✔
2197
            value = self.generate()
×
2198
        return value
2✔
2199

2200

2201
class UUIDField(BaseField):
2✔
2202
    """A UUID field."""
2203

2204
    _binary = None
2✔
2205

2206
    def __init__(self, binary=True, **kwargs):
2✔
2207
        """
2208
        Store UUID data in the database
2209

2210
        :param binary: if False store as a string.
2211
        """
2212
        self._binary = binary
2✔
2213
        super().__init__(**kwargs)
2✔
2214

2215
    def to_python(self, value):
2✔
2216
        if not self._binary:
2✔
2217
            original_value = value
2✔
2218
            try:
2✔
2219
                if not isinstance(value, str):
2✔
2220
                    value = str(value)
2✔
2221
                return uuid.UUID(value)
2✔
2222
            except (ValueError, TypeError, AttributeError):
×
2223
                return original_value
×
2224
        return value
2✔
2225

2226
    def to_mongo(self, value):
2✔
2227
        if not self._binary:
2✔
2228
            return str(value)
2✔
2229
        elif isinstance(value, str):
2✔
2230
            return uuid.UUID(value)
×
2231
        return value
2✔
2232

2233
    def prepare_query_value(self, op, value):
2✔
2234
        if value is None:
2✔
2235
            return None
×
2236
        return self.to_mongo(value)
2✔
2237

2238
    def validate(self, value):
2✔
2239
        if not isinstance(value, uuid.UUID):
2✔
2240
            if not isinstance(value, str):
2✔
2241
                value = str(value)
×
2242
            try:
2✔
2243
                uuid.UUID(value)
2✔
2244
            except (ValueError, TypeError, AttributeError) as exc:
2✔
2245
                self.error("Could not convert to UUID: %s" % exc)
2✔
2246

2247

2248
class GeoPointField(BaseField):
2✔
2249
    """A list storing a longitude and latitude coordinate.
2250

2251
    .. note:: this represents a generic point in a 2D plane and a legacy way of
2252
        representing a geo point. It admits 2d indexes but not "2dsphere" indexes
2253
        in MongoDB > 2.4 which are more natural for modeling geospatial points.
2254
        See :ref:`geospatial-indexes`
2255
    """
2256

2257
    _geo_index = pymongo.GEO2D
2✔
2258

2259
    def validate(self, value):
2✔
2260
        """Make sure that a geo-value is of type (x, y)"""
2261
        if not isinstance(value, (list, tuple)):
2✔
2262
            self.error("GeoPointField can only accept tuples or lists of (x, y)")
2✔
2263

2264
        if not len(value) == 2:
2✔
2265
            self.error("Value (%s) must be a two-dimensional point" % repr(value))
2✔
2266
        elif not isinstance(value[0], (float, int)) or not isinstance(
2✔
2267
            value[1], (float, int)
2268
        ):
2269
            self.error("Both values (%s) in point must be float or int" % repr(value))
2✔
2270

2271

2272
class PointField(GeoJsonBaseField):
2✔
2273
    """A GeoJSON field storing a longitude and latitude coordinate.
2274

2275
    The data is represented as:
2276

2277
    .. code-block:: js
2278

2279
        {'type' : 'Point' ,
2280
         'coordinates' : [x, y]}
2281

2282
    You can either pass a dict with the full information or a list
2283
    to set the value.
2284

2285
    Requires mongodb >= 2.4
2286
    """
2287

2288
    _type = "Point"
2✔
2289

2290

2291
class LineStringField(GeoJsonBaseField):
2✔
2292
    """A GeoJSON field storing a line of longitude and latitude coordinates.
2293

2294
    The data is represented as:
2295

2296
    .. code-block:: js
2297

2298
        {'type' : 'LineString' ,
2299
         'coordinates' : [[x1, y1], [x2, y2] ... [xn, yn]]}
2300

2301
    You can either pass a dict with the full information or a list of points.
2302

2303
    Requires mongodb >= 2.4
2304
    """
2305

2306
    _type = "LineString"
2✔
2307

2308

2309
class PolygonField(GeoJsonBaseField):
2✔
2310
    """A GeoJSON field storing a polygon of longitude and latitude coordinates.
2311

2312
    The data is represented as:
2313

2314
    .. code-block:: js
2315

2316
        {'type' : 'Polygon' ,
2317
         'coordinates' : [[[x1, y1], [x1, y1] ... [xn, yn]],
2318
                          [[x1, y1], [x1, y1] ... [xn, yn]]}
2319

2320
    You can either pass a dict with the full information or a list
2321
    of LineStrings. The first LineString being the outside and the rest being
2322
    holes.
2323

2324
    Requires mongodb >= 2.4
2325
    """
2326

2327
    _type = "Polygon"
2✔
2328

2329

2330
class MultiPointField(GeoJsonBaseField):
2✔
2331
    """A GeoJSON field storing a list of Points.
2332

2333
    The data is represented as:
2334

2335
    .. code-block:: js
2336

2337
        {'type' : 'MultiPoint' ,
2338
         'coordinates' : [[x1, y1], [x2, y2]]}
2339

2340
    You can either pass a dict with the full information or a list
2341
    to set the value.
2342

2343
    Requires mongodb >= 2.6
2344
    """
2345

2346
    _type = "MultiPoint"
2✔
2347

2348

2349
class MultiLineStringField(GeoJsonBaseField):
2✔
2350
    """A GeoJSON field storing a list of LineStrings.
2351

2352
    The data is represented as:
2353

2354
    .. code-block:: js
2355

2356
        {'type' : 'MultiLineString' ,
2357
         'coordinates' : [[[x1, y1], [x1, y1] ... [xn, yn]],
2358
                          [[x1, y1], [x1, y1] ... [xn, yn]]]}
2359

2360
    You can either pass a dict with the full information or a list of points.
2361

2362
    Requires mongodb >= 2.6
2363
    """
2364

2365
    _type = "MultiLineString"
2✔
2366

2367

2368
class MultiPolygonField(GeoJsonBaseField):
2✔
2369
    """A GeoJSON field storing  list of Polygons.
2370

2371
    The data is represented as:
2372

2373
    .. code-block:: js
2374

2375
        {'type' : 'MultiPolygon' ,
2376
         'coordinates' : [[
2377
               [[x1, y1], [x1, y1] ... [xn, yn]],
2378
               [[x1, y1], [x1, y1] ... [xn, yn]]
2379
           ], [
2380
               [[x1, y1], [x1, y1] ... [xn, yn]],
2381
               [[x1, y1], [x1, y1] ... [xn, yn]]
2382
           ]
2383
        }
2384

2385
    You can either pass a dict with the full information or a list
2386
    of Polygons.
2387

2388
    Requires mongodb >= 2.6
2389
    """
2390

2391
    _type = "MultiPolygon"
2✔
2392

2393

2394
class LazyReferenceField(BaseField):
2✔
2395
    """A really lazy reference to a document.
2396
    Unlike the :class:`~mongoengine.fields.ReferenceField` it will
2397
    **not** be automatically (lazily) dereferenced on access.
2398
    Instead, access will return a :class:`~mongoengine.base.LazyReference` class
2399
    instance, allowing access to `pk` or manual dereference by using
2400
    ``fetch()`` method.
2401
    """
2402

2403
    def __init__(
2✔
2404
        self,
2405
        document_type,
2406
        passthrough=False,
2407
        dbref=False,
2408
        reverse_delete_rule=DO_NOTHING,
2409
        **kwargs,
2410
    ):
2411
        """Initialises the Reference Field.
2412

2413
        :param dbref:  Store the reference as :class:`~pymongo.dbref.DBRef`
2414
          or as the :class:`~pymongo.objectid.ObjectId`.id .
2415
        :param reverse_delete_rule: Determines what to do when the referring
2416
          object is deleted
2417
        :param passthrough: When trying to access unknown fields, the
2418
          :class:`~mongoengine.base.datastructure.LazyReference` instance will
2419
          automatically call `fetch()` and try to retrieve the field on the fetched
2420
          document. Note this only work getting field (not setting or deleting).
2421
        """
2422
        # XXX ValidationError raised outside of the "validate" method.
2423
        if not isinstance(document_type, str) and not issubclass(
2✔
2424
            document_type, Document
2425
        ):
2426
            self.error(
2✔
2427
                "Argument to LazyReferenceField constructor must be a "
2428
                "document class or a string"
2429
            )
2430

2431
        self.dbref = dbref
2✔
2432
        self.passthrough = passthrough
2✔
2433
        self.document_type_obj = document_type
2✔
2434
        self.reverse_delete_rule = reverse_delete_rule
2✔
2435
        super().__init__(**kwargs)
2✔
2436

2437
    @property
2✔
2438
    def document_type(self):
2439
        if isinstance(self.document_type_obj, str):
2✔
2440
            if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
2✔
2441
                self.document_type_obj = self.owner_document
×
2442
            else:
2443
                self.document_type_obj = get_document(self.document_type_obj)
2✔
2444
        return self.document_type_obj
2✔
2445

2446
    def build_lazyref(self, value):
2✔
2447
        if isinstance(value, LazyReference):
2✔
2448
            if value.passthrough != self.passthrough:
2✔
2449
                value = LazyReference(
×
2450
                    value.document_type, value.pk, passthrough=self.passthrough
2451
                )
2452
        elif value is not None:
2✔
2453
            if isinstance(value, self.document_type):
2✔
2454
                value = LazyReference(
2✔
2455
                    self.document_type, value.pk, passthrough=self.passthrough
2456
                )
2457
            elif isinstance(value, DBRef):
2✔
2458
                value = LazyReference(
2✔
2459
                    self.document_type, value.id, passthrough=self.passthrough
2460
                )
2461
            else:
2462
                # value is the primary key of the referenced document
2463
                value = LazyReference(
2✔
2464
                    self.document_type, value, passthrough=self.passthrough
2465
                )
2466
        return value
2✔
2467

2468
    def __get__(self, instance, owner):
2✔
2469
        """Descriptor to allow lazy dereferencing."""
2470
        if instance is None:
2✔
2471
            # Document class being used rather than a document object
2472
            return self
×
2473

2474
        value = self.build_lazyref(instance._data.get(self.name))
2✔
2475
        if value:
2✔
2476
            instance._data[self.name] = value
2✔
2477

2478
        return super().__get__(instance, owner)
2✔
2479

2480
    def to_mongo(self, value):
2✔
2481
        if isinstance(value, LazyReference):
2✔
2482
            pk = value.pk
2✔
2483
        elif isinstance(value, self.document_type):
2✔
2484
            pk = value.pk
2✔
2485
        elif isinstance(value, DBRef):
2✔
2486
            pk = value.id
2✔
2487
        else:
2488
            # value is the primary key of the referenced document
2489
            pk = value
×
2490
        id_field_name = self.document_type._meta["id_field"]
2✔
2491
        id_field = self.document_type._fields[id_field_name]
2✔
2492
        pk = id_field.to_mongo(pk)
2✔
2493
        if self.dbref:
2✔
2494
            return DBRef(self.document_type._get_collection_name(), pk)
2✔
2495
        else:
2496
            return pk
2✔
2497

2498
    def to_python(self, value):
2✔
2499
        """Convert a MongoDB-compatible type to a Python type."""
2500
        if not isinstance(value, (DBRef, Document, EmbeddedDocument)):
2✔
2501
            collection = self.document_type._get_collection_name()
2✔
2502
            value = DBRef(collection, self.document_type.id.to_python(value))
2✔
2503
            value = self.build_lazyref(value)
2✔
2504
        return value
2✔
2505

2506
    def validate(self, value):
2✔
2507
        if isinstance(value, LazyReference):
2✔
2508
            if value.collection != self.document_type._get_collection_name():
2✔
2509
                self.error("Reference must be on a `%s` document." % self.document_type)
2✔
2510
            pk = value.pk
2✔
2511
        elif isinstance(value, self.document_type):
2✔
2512
            pk = value.pk
2✔
2513
        elif isinstance(value, DBRef):
2✔
2514
            # TODO: check collection ?
2515
            collection = self.document_type._get_collection_name()
2✔
2516
            if value.collection != collection:
2✔
2517
                self.error("DBRef on bad collection (must be on `%s`)" % collection)
2✔
2518
            pk = value.id
2✔
2519
        else:
2520
            # value is the primary key of the referenced document
2521
            id_field_name = self.document_type._meta["id_field"]
2✔
2522
            id_field = getattr(self.document_type, id_field_name)
2✔
2523
            pk = value
2✔
2524
            try:
2✔
2525
                id_field.validate(pk)
2✔
2526
            except ValidationError:
2✔
2527
                self.error(
2✔
2528
                    "value should be `{0}` document, LazyReference or DBRef on `{0}` "
2529
                    "or `{0}`'s primary key (i.e. `{1}`)".format(
2530
                        self.document_type.__name__, type(id_field).__name__
2531
                    )
2532
                )
2533

2534
        if pk is None:
2✔
2535
            self.error(
2✔
2536
                "You can only reference documents once they have been "
2537
                "saved to the database"
2538
            )
2539

2540
    def prepare_query_value(self, op, value):
2✔
2541
        if value is None:
2✔
2542
            return None
×
2543
        super().prepare_query_value(op, value)
2✔
2544
        return self.to_mongo(value)
2✔
2545

2546
    def lookup_member(self, member_name):
2✔
2547
        return self.document_type._fields.get(member_name)
×
2548

2549

2550
class GenericLazyReferenceField(GenericReferenceField):
2✔
2551
    """A reference to *any* :class:`~mongoengine.document.Document` subclass.
2552
    Unlike the :class:`~mongoengine.fields.GenericReferenceField` it will
2553
    **not** be automatically (lazily) dereferenced on access.
2554
    Instead, access will return a :class:`~mongoengine.base.LazyReference` class
2555
    instance, allowing access to `pk` or manual dereference by using
2556
    ``fetch()`` method.
2557

2558
    .. note ::
2559
        * Any documents used as a generic reference must be registered in the
2560
          document registry.  Importing the model will automatically register
2561
          it.
2562

2563
        * You can use the choices param to limit the acceptable Document types
2564
    """
2565

2566
    def __init__(self, *args, **kwargs):
2✔
2567
        self.passthrough = kwargs.pop("passthrough", False)
2✔
2568
        super().__init__(*args, **kwargs)
2✔
2569

2570
    def _validate_choices(self, value):
2✔
2571
        if isinstance(value, LazyReference):
2✔
2572
            value = value.document_type._class_name
2✔
2573
        super()._validate_choices(value)
2✔
2574

2575
    def build_lazyref(self, value):
2✔
2576
        if isinstance(value, LazyReference):
2✔
2577
            if value.passthrough != self.passthrough:
2✔
2578
                value = LazyReference(
×
2579
                    value.document_type, value.pk, passthrough=self.passthrough
2580
                )
2581
        elif value is not None:
2✔
2582
            if isinstance(value, (dict, SON)):
2✔
2583
                value = LazyReference(
2✔
2584
                    get_document(value["_cls"]),
2585
                    value["_ref"].id,
2586
                    passthrough=self.passthrough,
2587
                )
2588
            elif isinstance(value, Document):
2✔
2589
                value = LazyReference(
2✔
2590
                    type(value), value.pk, passthrough=self.passthrough
2591
                )
2592
        return value
2✔
2593

2594
    def __get__(self, instance, owner):
2✔
2595
        if instance is None:
2✔
2596
            return self
×
2597

2598
        value = self.build_lazyref(instance._data.get(self.name))
2✔
2599
        if value:
2✔
2600
            instance._data[self.name] = value
2✔
2601

2602
        return super().__get__(instance, owner)
2✔
2603

2604
    def validate(self, value):
2✔
2605
        if isinstance(value, LazyReference) and value.pk is None:
2✔
2606
            self.error(
×
2607
                "You can only reference documents once they have been"
2608
                " saved to the database"
2609
            )
2610
        return super().validate(value)
2✔
2611

2612
    def to_mongo(self, document):
2✔
2613
        if document is None:
2✔
2614
            return None
×
2615

2616
        if isinstance(document, LazyReference):
2✔
2617
            return SON(
2✔
2618
                (
2619
                    ("_cls", document.document_type._class_name),
2620
                    (
2621
                        "_ref",
2622
                        DBRef(
2623
                            document.document_type._get_collection_name(), document.pk
2624
                        ),
2625
                    ),
2626
                )
2627
            )
2628
        else:
2629
            return super().to_mongo(document)
2✔
2630

2631

2632
class Decimal128Field(BaseField):
2✔
2633
    """
2634
    128-bit decimal-based floating-point field capable of emulating decimal
2635
    rounding with exact precision. This field will expose decimal.Decimal but stores the value as a
2636
    `bson.Decimal128` behind the scene, this field is intended for monetary data, scientific computations, etc.
2637
    """
2638

2639
    DECIMAL_CONTEXT = create_decimal128_context()
2✔
2640

2641
    def __init__(self, min_value=None, max_value=None, **kwargs):
2✔
2642
        self.min_value = min_value
2✔
2643
        self.max_value = max_value
2✔
2644
        super().__init__(**kwargs)
2✔
2645

2646
    def to_mongo(self, value):
2✔
2647
        if value is None:
2✔
2648
            return None
2✔
2649
        if isinstance(value, Decimal128):
2✔
2650
            return value
2✔
2651
        if not isinstance(value, decimal.Decimal):
2✔
2652
            with decimal.localcontext(self.DECIMAL_CONTEXT) as ctx:
2✔
2653
                value = ctx.create_decimal(value)
2✔
2654
        return Decimal128(value)
2✔
2655

2656
    def to_python(self, value):
2✔
2657
        if value is None:
2✔
2658
            return None
×
2659
        return self.to_mongo(value).to_decimal()
2✔
2660

2661
    def validate(self, value):
2✔
2662
        if not isinstance(value, Decimal128):
2✔
2663
            try:
2✔
2664
                value = Decimal128(value)
2✔
2665
            except (TypeError, ValueError, decimal.InvalidOperation) as exc:
2✔
2666
                self.error("Could not convert value to Decimal128: %s" % exc)
2✔
2667

2668
        if self.min_value is not None and value.to_decimal() < self.min_value:
2✔
2669
            self.error("Decimal value is too small")
2✔
2670

2671
        if self.max_value is not None and value.to_decimal() > self.max_value:
2✔
2672
            self.error("Decimal value is too large")
2✔
2673

2674
    def prepare_query_value(self, op, value):
2✔
2675
        return super().prepare_query_value(op, self.to_mongo(value))
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc