morango 0.6.14__py2.py3-none-any.whl → 0.8.6__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. morango/__init__.py +1 -6
  2. morango/api/serializers.py +3 -0
  3. morango/api/viewsets.py +19 -6
  4. morango/apps.py +1 -2
  5. morango/constants/transfer_stages.py +1 -1
  6. morango/constants/transfer_statuses.py +1 -1
  7. morango/management/commands/cleanupsyncs.py +64 -14
  8. morango/migrations/0001_initial.py +0 -2
  9. morango/migrations/0001_squashed_0024_auto_20240129_1757.py +583 -0
  10. morango/migrations/0002_auto_20170511_0400.py +0 -2
  11. morango/migrations/0002_store_idx_morango_deserialize.py +21 -0
  12. morango/migrations/0003_auto_20170519_0543.py +0 -2
  13. morango/migrations/0004_auto_20170520_2112.py +0 -2
  14. morango/migrations/0005_auto_20170629_2139.py +0 -2
  15. morango/migrations/0006_instanceidmodel_system_id.py +0 -2
  16. morango/migrations/0007_auto_20171018_1615.py +0 -2
  17. morango/migrations/0008_auto_20171114_2217.py +0 -2
  18. morango/migrations/0009_auto_20171205_0252.py +0 -2
  19. morango/migrations/0010_auto_20171206_1615.py +0 -2
  20. morango/migrations/0011_sharedkey.py +0 -2
  21. morango/migrations/0012_auto_20180927_1658.py +0 -2
  22. morango/migrations/0013_auto_20190627_1513.py +0 -2
  23. morango/migrations/0014_syncsession_extra_fields.py +0 -2
  24. morango/migrations/0015_auto_20200508_2104.py +2 -3
  25. morango/migrations/0016_store_deserialization_error.py +2 -3
  26. morango/migrations/0017_store_last_transfer_session_id.py +1 -2
  27. morango/migrations/0018_auto_20210714_2216.py +2 -3
  28. morango/migrations/0019_auto_20220113_1807.py +2 -3
  29. morango/migrations/0020_postgres_fix_nullable.py +0 -2
  30. morango/migrations/0021_store_partition_index_create.py +0 -2
  31. morango/migrations/0022_rename_instance_fields.py +23 -0
  32. morango/migrations/0023_add_instance_id_fields.py +24 -0
  33. morango/migrations/0024_auto_20240129_1757.py +28 -0
  34. morango/models/__init__.py +0 -6
  35. morango/models/certificates.py +137 -28
  36. morango/models/core.py +36 -36
  37. morango/models/fields/crypto.py +20 -6
  38. morango/models/fields/uuids.py +2 -1
  39. morango/models/utils.py +5 -6
  40. morango/proquint.py +2 -3
  41. morango/registry.py +23 -47
  42. morango/sync/backends/utils.py +10 -11
  43. morango/sync/context.py +48 -40
  44. morango/sync/controller.py +10 -1
  45. morango/sync/operations.py +1 -1
  46. morango/sync/session.py +11 -0
  47. morango/sync/syncsession.py +41 -22
  48. morango/urls.py +3 -3
  49. morango/utils.py +2 -3
  50. {morango-0.6.14.dist-info → morango-0.8.6.dist-info}/METADATA +29 -14
  51. morango-0.8.6.dist-info/RECORD +79 -0
  52. {morango-0.6.14.dist-info → morango-0.8.6.dist-info}/WHEEL +1 -1
  53. morango/models/morango_mptt.py +0 -33
  54. morango/settings.py +0 -115
  55. morango/wsgi.py +0 -33
  56. morango-0.6.14.dist-info/RECORD +0 -77
  57. {morango-0.6.14.dist-info → morango-0.8.6.dist-info/licenses}/AUTHORS.md +0 -0
  58. {morango-0.6.14.dist-info → morango-0.8.6.dist-info/licenses}/LICENSE +0 -0
  59. {morango-0.6.14.dist-info → morango-0.8.6.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.15 on 2018-09-27 16:58
3
- from __future__ import unicode_literals
4
-
5
3
  from django.db import migrations
6
4
  from django.db import models
7
5
 
@@ -1,7 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.21 on 2019-06-27 22:13
3
- from __future__ import unicode_literals
4
-
5
3
  from django.db import migrations
6
4
 
7
5
  import morango.models.fields.uuids
@@ -1,7 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.27 on 2019-12-30 18:28
3
- from __future__ import unicode_literals
4
-
5
3
  from django.db import migrations
6
4
  from django.db import models
7
5
 
@@ -1,8 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.29 on 2020-05-08 21:04
3
- from __future__ import unicode_literals
4
-
5
- from django.db import migrations, models
3
+ from django.db import migrations
4
+ from django.db import models
6
5
 
7
6
 
8
7
  class Migration(migrations.Migration):
@@ -1,8 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.28 on 2020-06-10 23:48
3
- from __future__ import unicode_literals
4
-
5
- from django.db import migrations, models
3
+ from django.db import migrations
4
+ from django.db import models
6
5
 
7
6
 
8
7
  class Migration(migrations.Migration):
@@ -1,8 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.28 on 2021-06-25 23:13
3
- from __future__ import unicode_literals
4
-
5
3
  from django.db import migrations
4
+
6
5
  import morango.models.fields.uuids
7
6
 
8
7
 
@@ -1,8 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.29 on 2021-07-14 22:16
3
- from __future__ import unicode_literals
4
-
5
- from django.db import migrations, models
3
+ from django.db import migrations
4
+ from django.db import models
6
5
 
7
6
 
8
7
  class Migration(migrations.Migration):
@@ -1,8 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.29 on 2022-01-13 18:07
3
- from __future__ import unicode_literals
4
-
5
- from django.db import migrations, models
3
+ from django.db import migrations
4
+ from django.db import models
6
5
 
7
6
 
8
7
  class Migration(migrations.Migration):
@@ -1,7 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.29 on 2022-01-13 18:07
3
- from __future__ import unicode_literals
4
-
5
3
  from django.db import migrations
6
4
 
7
5
 
@@ -1,7 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.29 on 2022-04-27 16:59
3
- from __future__ import unicode_literals
4
-
5
3
  from django.db import migrations
6
4
  from django.db import models
7
5
 
@@ -0,0 +1,23 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by Django 1.11.29 on 2023-01-31 18:53
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('morango', '0021_store_partition_index_create'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RenameField(
14
+ model_name='syncsession',
15
+ old_name='client_instance',
16
+ new_name='client_instance_json',
17
+ ),
18
+ migrations.RenameField(
19
+ model_name='syncsession',
20
+ old_name='server_instance',
21
+ new_name='server_instance_json',
22
+ ),
23
+ ]
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by Django 1.11.29 on 2023-01-31 19:03
3
+ from django.db import migrations
4
+ from django.db import models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('morango', '0022_rename_instance_fields'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name='syncsession',
16
+ name='client_instance_id',
17
+ field=models.UUIDField(blank=True, null=True),
18
+ ),
19
+ migrations.AddField(
20
+ model_name='syncsession',
21
+ name='server_instance_id',
22
+ field=models.UUIDField(blank=True, null=True),
23
+ ),
24
+ ]
@@ -0,0 +1,28 @@
1
+ # Generated by Django 3.2.23 on 2024-01-29 17:57
2
+ from django.db import migrations
3
+ from django.db import models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('morango', '0023_add_instance_id_fields'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='certificate',
15
+ name='level',
16
+ field=models.PositiveIntegerField(editable=False),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='certificate',
20
+ name='lft',
21
+ field=models.PositiveIntegerField(editable=False),
22
+ ),
23
+ migrations.AlterField(
24
+ model_name='certificate',
25
+ name='rght',
26
+ field=models.PositiveIntegerField(editable=False),
27
+ ),
28
+ ]
@@ -21,9 +21,6 @@ from morango.models.fields import __all__ as fields_all
21
21
  from morango.models.fields.crypto import SharedKey
22
22
  from morango.models.fields.uuids import UUIDModelMixin
23
23
  from morango.models.manager import SyncableModelManager
24
- from morango.models.morango_mptt import MorangoMPTTModel
25
- from morango.models.morango_mptt import MorangoMPTTTreeManager
26
- from morango.models.morango_mptt import MorangoTreeQuerySet
27
24
  from morango.models.query import SyncableModelQuerySet
28
25
  from morango.registry import syncable_models
29
26
 
@@ -41,9 +38,6 @@ __all__ += [
41
38
  "SyncableModelManager",
42
39
  "SyncableModelQuerySet",
43
40
  "syncable_models",
44
- "MorangoTreeQuerySet",
45
- "MorangoMPTTTreeManager",
46
- "MorangoMPTTModel",
47
41
  "DatabaseIDModel",
48
42
  "InstanceIDModel",
49
43
  "SyncSession",
@@ -3,18 +3,18 @@
3
3
  Each certificate has a ``private_key`` used for signing (child) certificates (thus giving certain permissions)
4
4
  and a ``public_key`` used for verifying that a certificate(s) was properly signed.
5
5
  """
6
- from __future__ import unicode_literals
7
-
8
6
  import json
7
+ import logging
9
8
  import string
9
+ from contextlib import contextmanager
10
10
 
11
11
  import mptt.models
12
12
  from django.core.management import call_command
13
+ from django.db import connection
13
14
  from django.db import models
14
15
  from django.db import transaction
16
+ from django.db.utils import OperationalError
15
17
  from django.utils import timezone
16
- from django.utils.six import string_types
17
- from future.utils import python_2_unicode_compatible
18
18
 
19
19
  from .fields.crypto import Key
20
20
  from .fields.crypto import PrivateKeyField
@@ -27,21 +27,21 @@ from morango.errors import CertificateScopeNotSubset
27
27
  from morango.errors import CertificateSignatureInvalid
28
28
  from morango.errors import NonceDoesNotExist
29
29
  from morango.errors import NonceExpired
30
+ from morango.sync.backends.utils import load_backend
30
31
  from morango.utils import _assert
31
32
 
32
33
 
33
- @python_2_unicode_compatible
34
34
  class Certificate(mptt.models.MPTTModel, UUIDModelMixin):
35
35
 
36
36
  uuid_input_fields = ("public_key", "profile", "salt")
37
37
 
38
- parent = models.ForeignKey("Certificate", blank=True, null=True)
38
+ parent = models.ForeignKey("Certificate", blank=True, null=True, on_delete=models.CASCADE)
39
39
 
40
40
  # the Morango profile with which this certificate is associated
41
41
  profile = models.CharField(max_length=20)
42
42
 
43
43
  # scope of this certificate, and version of the scope, along with associated params
44
- scope_definition = models.ForeignKey("ScopeDefinition")
44
+ scope_definition = models.ForeignKey("ScopeDefinition", on_delete=models.CASCADE)
45
45
  scope_version = models.IntegerField()
46
46
  scope_params = (
47
47
  models.TextField()
@@ -199,7 +199,7 @@ class Certificate(mptt.models.MPTTModel, UUIDModelMixin):
199
199
  def save_certificate_chain(cls, cert_chain, expected_last_id=None):
200
200
 
201
201
  # parse the chain from json if needed
202
- if isinstance(cert_chain, string_types):
202
+ if isinstance(cert_chain, str):
203
203
  cert_chain = json.loads(cert_chain)
204
204
 
205
205
  # start from the bottom of the chain
@@ -251,6 +251,37 @@ class Certificate(mptt.models.MPTTModel, UUIDModelMixin):
251
251
  def get_scope(self):
252
252
  return self.scope_definition.get_scope(self.scope_params)
253
253
 
254
+ @contextmanager
255
+ def _attempt_lock_mptt(self):
256
+ from morango.sync.utils import lock_partitions
257
+
258
+ DBBackend = load_backend(connection)
259
+
260
+ with transaction.atomic():
261
+ # Call get_root on the parent as it is already saved in the DB
262
+ root_id = self.parent.get_root().id if self.parent else self.id
263
+
264
+ # lock the partitions in our scope to prevent MPTT tree corruption during concurrent certificate creation
265
+ lock_partitions(DBBackend, sync_filter=Filter(root_id) if root_id else None)
266
+ yield
267
+
268
+ @contextmanager
269
+ def _lock_mptt(self):
270
+ try:
271
+ with self._attempt_lock_mptt():
272
+ yield
273
+ except OperationalError as e:
274
+ if "deadlock detected" in e.args[0]:
275
+ logging.error("Deadlock detected when attempting to lock MPTT partitions, retrying once more")
276
+ with self._attempt_lock_mptt():
277
+ yield
278
+ else:
279
+ raise
280
+
281
+ def save(self, *args, **kwargs):
282
+ with self._lock_mptt():
283
+ super().save(*args, **kwargs)
284
+
254
285
  def __str__(self):
255
286
  if self.scope_definition:
256
287
  return self.scope_definition.get_description(self.scope_params)
@@ -324,52 +355,95 @@ class ScopeDefinition(models.Model):
324
355
  return Scope(definition=self, params=params)
325
356
 
326
357
  def get_description(self, params):
327
- if isinstance(params, string_types):
358
+ if isinstance(params, str):
328
359
  params = json.loads(params)
329
360
  return string.Template(self.description).safe_substitute(params)
330
361
 
331
362
 
332
- @python_2_unicode_compatible
333
363
  class Filter(object):
334
- def __init__(self, template, params={}):
335
- # ensure params have been deserialized
336
- if isinstance(params, string_types):
337
- params = json.loads(params)
338
- self._template = template
339
- self._params = params
340
- self._filter_string = string.Template(template).safe_substitute(params)
341
- self._filter_tuple = tuple(self._filter_string.split()) or ("",)
364
+ def __init__(self, filter_str, params=None):
365
+ """
366
+ :param filter_str: The partition filter string, which may have multiple separated by newlines
367
+ :type filter_str: str
368
+ :param params: DEPRECATED: USE Filter.from_template() INSTEAD
369
+ :type params: dict|str
370
+ """
371
+ if params is not None:
372
+ logging.warning("DEPRECATED: Constructing a filter with a template and params is deprecated. Use Filter.from_template() instead")
373
+ filter_str = str(Filter.from_template(filter_str, params=params))
374
+
375
+ self._filter_tuple = tuple(filter_str.split()) or ("",)
342
376
 
343
377
  def is_subset_of(self, other):
344
- for partition in self._filter_tuple:
345
- if not partition.startswith(other._filter_tuple):
378
+ """
379
+ :param other: The other Filter
380
+ :type other: Filter
381
+ :return: A boolean on whether this Filter is captured within the other Filter
382
+ :rtype: bool
383
+ """
384
+ for partition in self:
385
+ if not other.contains_partition(partition):
346
386
  return False
347
387
  return True
348
388
 
349
389
  def contains_partition(self, partition):
390
+ """Returns True if the partition starts with as least one of the partitions in this Filter"""
350
391
  return partition.startswith(self._filter_tuple)
351
392
 
393
+ def contains_exact_partition(self, partition):
394
+ """Returns True if the partition exactly matches one of the partitions in this Filter"""
395
+ return partition in self._filter_tuple
396
+
397
+ def copy(self):
398
+ return Filter(str(self))
399
+
352
400
  def __le__(self, other):
401
+ """Returns True if this Filter is a subset of the other"""
353
402
  return self.is_subset_of(other)
354
403
 
355
404
  def __eq__(self, other):
405
+ """Returns True if this Filter has exactly the same partitions as the other"""
356
406
  if other is None:
357
407
  return False
358
- for partition in self._filter_tuple:
359
- if partition not in other._filter_tuple:
408
+ for partition in self:
409
+ if not other.contains_exact_partition(partition):
360
410
  return False
361
- for partition in other._filter_tuple:
362
- if partition not in self._filter_tuple:
411
+ for partition in other:
412
+ if not self.contains_exact_partition(partition):
363
413
  return False
364
414
  return True
365
415
 
366
416
  def __contains__(self, partition):
417
+ """
418
+ Performs a 'startswith' comparison on the partition, determining whether it matches or
419
+ is a subset of any partition in this Filter
420
+
421
+ :param partition: str
422
+ :return: A boolean
423
+ :rtype: bool
424
+ """
367
425
  return self.contains_partition(partition)
368
426
 
369
427
  def __add__(self, other):
370
- return Filter(self._filter_string + "\n" + other._filter_string)
428
+ """
429
+ The Filter's addition operator overload
430
+ :param other: Filter or None
431
+ :type other: Filter|None
432
+ :return: The combined Filter
433
+ :rtype: Filter
434
+ """
435
+ if other is None:
436
+ return self
437
+ # create a list of partition filters, deduplicating them between the two filter objects
438
+ partitions = []
439
+ partitions.extend(p for p in self if p)
440
+ partitions.extend(p for p in other if p and p not in partitions)
441
+ return Filter("\n".join(partitions))
371
442
 
372
443
  def __iter__(self):
444
+ """
445
+ :rtype: tuple[str]
446
+ """
373
447
  return iter(self._filter_tuple)
374
448
 
375
449
  def __str__(self):
@@ -378,13 +452,48 @@ class Filter(object):
378
452
  def __len__(self):
379
453
  return len(self._filter_tuple)
380
454
 
455
+ @classmethod
456
+ def add(cls, filter_a, filter_b):
457
+ """
458
+ The Filter's addition operator overload is already defensive against None being the
459
+ right-hand operand, but this method is defensive against None being the left-hand operand
460
+
461
+ :param filter_a: A Filter or None
462
+ :type filter_a: Filter|None
463
+ :param filter_b: A Filter or None
464
+ :type filter_b: Filter|None
465
+ :return: The combined Filter or None
466
+ :rtype: Filter|None
467
+ """
468
+ if filter_a is None:
469
+ return filter_b
470
+ return filter_a + filter_b
471
+
472
+ @classmethod
473
+ def from_template(cls, template, params=None):
474
+ """
475
+ Create a filter from a string template, which may have params that will be replaced with
476
+ values passed to `params`
477
+
478
+ :param template: The partition filter template
479
+ :type template: str
480
+ :param params: The param dictionary or JSON object string
481
+ :type params: dict|str
482
+ :return: The filter with params replaced
483
+ :rtype: Filter
484
+ """
485
+ if isinstance(params, str):
486
+ params = json.loads(params)
487
+ params = params or {}
488
+ return Filter(string.Template(template).safe_substitute(params))
489
+
381
490
 
382
491
  class Scope(object):
383
492
  def __init__(self, definition, params):
384
493
  # turn the scope definition filter templates into Filter objects
385
- rw_filter = Filter(definition.read_write_filter_template, params)
386
- self.read_filter = rw_filter + Filter(definition.read_filter_template, params)
387
- self.write_filter = rw_filter + Filter(definition.write_filter_template, params)
494
+ rw_filter = Filter.from_template(definition.read_write_filter_template, params)
495
+ self.read_filter = rw_filter + Filter.from_template(definition.read_filter_template, params)
496
+ self.write_filter = rw_filter + Filter.from_template(definition.write_filter_template, params)
388
497
 
389
498
  def is_subset_of(self, other):
390
499
  if not self.read_filter.is_subset_of(other.read_filter):
morango/models/core.py CHANGED
@@ -1,5 +1,3 @@
1
- from __future__ import unicode_literals
2
-
3
1
  import functools
4
2
  import json
5
3
  import logging
@@ -17,13 +15,13 @@ from django.db.models import F
17
15
  from django.db.models import Func
18
16
  from django.db.models import Max
19
17
  from django.db.models import Q
18
+ from django.db.models import signals
20
19
  from django.db.models import TextField
21
20
  from django.db.models import Value
22
21
  from django.db.models.deletion import Collector
23
22
  from django.db.models.expressions import CombinedExpression
24
23
  from django.db.models.fields.related import ForeignKey
25
24
  from django.db.models.functions import Cast
26
- from django.utils import six
27
25
  from django.utils import timezone
28
26
  from django.utils.functional import cached_property
29
27
 
@@ -38,7 +36,6 @@ from morango.models.fields.uuids import UUIDField
38
36
  from morango.models.fields.uuids import UUIDModelMixin
39
37
  from morango.models.fsic_utils import remove_redundant_instance_counters
40
38
  from morango.models.manager import SyncableModelManager
41
- from morango.models.morango_mptt import MorangoMPTTModel
42
39
  from morango.models.utils import get_0_4_system_parameters
43
40
  from morango.models.utils import get_0_5_mac_address
44
41
  from morango.models.utils import get_0_5_system_id
@@ -240,9 +237,12 @@ class SyncSession(models.Model):
240
237
  client_ip = models.CharField(max_length=100, blank=True)
241
238
  server_ip = models.CharField(max_length=100, blank=True)
242
239
 
243
- # serialized copies of the client and server instance model fields, for debugging/tracking purposes
244
- client_instance = models.TextField(default="{}")
245
- server_instance = models.TextField(default="{}")
240
+ # track the instance IDs for convenient querying, as well as serialized copies of
241
+ # the client and server instance model fields for debugging/tracking purposes
242
+ client_instance_id = models.UUIDField(null=True, blank=True)
243
+ client_instance_json = models.TextField(default="{}")
244
+ server_instance_id = models.UUIDField(null=True, blank=True)
245
+ server_instance_json = models.TextField(default="{}")
246
246
 
247
247
  # used to store other data we may need to know about this sync session
248
248
  extra_fields = models.TextField(default="{}")
@@ -252,11 +252,11 @@ class SyncSession(models.Model):
252
252
 
253
253
  @cached_property
254
254
  def client_instance_data(self):
255
- return json.loads(self.client_instance)
255
+ return json.loads(self.client_instance_json)
256
256
 
257
257
  @cached_property
258
258
  def server_instance_data(self):
259
- return json.loads(self.server_instance)
259
+ return json.loads(self.server_instance_json)
260
260
 
261
261
 
262
262
  class TransferSession(models.Model):
@@ -351,10 +351,10 @@ class TransferSession(models.Model):
351
351
 
352
352
  def get_touched_record_ids_for_model(self, model):
353
353
  if isinstance(model, SyncableModel) or (
354
- isinstance(model, six.class_types) and issubclass(model, SyncableModel)
354
+ isinstance(model, type) and issubclass(model, SyncableModel)
355
355
  ):
356
356
  model = model.morango_model_name
357
- _assert(isinstance(model, six.string_types), "Model must resolve to string")
357
+ _assert(isinstance(model, str), "Model must resolve to string")
358
358
  return Store.objects.filter(
359
359
  model_name=model, last_transfer_session_id=self.id
360
360
  ).values_list("id", flat=True)
@@ -418,7 +418,7 @@ class StoreQueryset(models.QuerySet):
418
418
  self.annotate(id_cast=Cast("id", TextField()))
419
419
  # remove dashes from char uuid
420
420
  .annotate(
421
- fixed_id=Func(F("id_cast"), Value("-"), Value(""), function="replace")
421
+ fixed_id=Func(F("id_cast"), Value("-"), Value(""), function="replace", output_field=TextField())
422
422
  )
423
423
  # return as list
424
424
  .values_list("fixed_id", flat=True)
@@ -450,6 +450,7 @@ class Store(AbstractStore):
450
450
  class Meta:
451
451
  indexes = [
452
452
  models.Index(fields=["partition"], name="idx_morango_store_partition"),
453
+ models.Index(fields=["profile", "model_name", "partition", "dirty_bit"], condition=models.Q(dirty_bit=True), name="idx_morango_deserialize"),
453
454
  ]
454
455
 
455
456
  def _deserialize_store_model(self, fk_cache, defer_fks=False): # noqa: C901
@@ -465,14 +466,14 @@ class Store(AbstractStore):
465
466
  klass_model = syncable_models.get_model(self.profile, self.model_name)
466
467
  # if store model marked as deleted, attempt to delete in app layer
467
468
  if self.deleted:
468
- # if hard deleted, propagate to related models
469
- if self.hard_deleted:
470
- try:
471
- klass_model.objects.get(id=self.id).delete(hard_delete=True)
472
- except klass_model.DoesNotExist:
473
- pass
474
- else:
475
- klass_model.objects.filter(id=self.id).delete()
469
+ # Don't differentiate between deletion and hard deletion here,
470
+ # as we don't want to add additional tracking for models in either case,
471
+ # just to actually delete them.
472
+ # Import here to avoid circular import, as the utils module
473
+ # imports core models.
474
+ from morango.sync.utils import mute_signals
475
+ with mute_signals(signals.post_delete):
476
+ klass_model.syncing_objects.filter(id=self.id).delete()
476
477
  return None, deferred_fks
477
478
  else:
478
479
  # load model into memory
@@ -608,7 +609,7 @@ class DatabaseMaxCounter(AbstractCounter):
608
609
  )
609
610
  else:
610
611
  updated_fsic = {}
611
- for key, value in six.iteritems(fsics):
612
+ for key, value in fsics.items():
612
613
  if key in internal_fsic:
613
614
  # if same instance id, update fsic with larger value
614
615
  if fsics[key] > internal_fsic[key]:
@@ -618,7 +619,7 @@ class DatabaseMaxCounter(AbstractCounter):
618
619
  updated_fsic[key] = fsics[key]
619
620
 
620
621
  # load database max counters
621
- for (key, value) in six.iteritems(updated_fsic):
622
+ for (key, value) in updated_fsic.items():
622
623
  for f in sync_filter:
623
624
  DatabaseMaxCounter.objects.update_or_create(
624
625
  instance_id=key, partition=f, defaults={"counter": value}
@@ -805,6 +806,11 @@ class SyncableModel(UUIDModelMixin):
805
806
 
806
807
  objects = SyncableModelManager()
807
808
 
809
+ # Add a special syncing_objects queryset to every SyncableModel for use in syncing operations.
810
+ # This means that we still deal with the entire set of objects when syncing, even if the default
811
+ # model manager has been overridden to filter the queryset.
812
+ syncing_objects = SyncableModelManager()
813
+
808
814
  class Meta:
809
815
  abstract = True
810
816
 
@@ -841,10 +847,8 @@ class SyncableModel(UUIDModelMixin):
841
847
  with transaction.atomic():
842
848
  if hard_delete:
843
849
  # set hard deletion for all related models
844
- for model, instances in six.iteritems(collector.data):
845
- if issubclass(model, SyncableModel) or issubclass(
846
- model, MorangoMPTTModel
847
- ):
850
+ for model, instances in collector.data.items():
851
+ if issubclass(model, SyncableModel):
848
852
  for obj in instances:
849
853
  obj._update_hard_deleted_models()
850
854
  return collector.delete()
@@ -928,15 +932,8 @@ class SyncableModel(UUIDModelMixin):
928
932
  continue
929
933
  if f.attname in self._morango_internal_fields_not_to_serialize:
930
934
  continue
931
- # case if model is morango mptt
932
- if f.attname in getattr(
933
- self,
934
- "_internal_mptt_fields_not_to_serialize",
935
- "_internal_fields_not_to_serialize",
936
- ):
937
- continue
938
- if hasattr(f, "value_from_object_json_compatible"):
939
- data[f.attname] = f.value_from_object_json_compatible(self)
935
+ if getattr(f, "morango_serialize_to_string", False):
936
+ data[f.attname] = f.value_to_string(self)
940
937
  else:
941
938
  data[f.attname] = f.value_from_object(self)
942
939
  return data
@@ -947,7 +944,10 @@ class SyncableModel(UUIDModelMixin):
947
944
  kwargs = {}
948
945
  for f in cls._meta.concrete_fields:
949
946
  if f.attname in dict_model:
950
- kwargs[f.attname] = dict_model[f.attname]
947
+ if getattr(f, "morango_serialize_to_string", False):
948
+ kwargs[f.attname] = f.to_python(dict_model[f.attname])
949
+ else:
950
+ kwargs[f.attname] = dict_model[f.attname]
951
951
  return cls(**kwargs)
952
952
 
953
953
  @classmethod