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.
- morango/__init__.py +1 -6
- morango/api/serializers.py +3 -0
- morango/api/viewsets.py +19 -6
- morango/apps.py +1 -2
- morango/constants/transfer_stages.py +1 -1
- morango/constants/transfer_statuses.py +1 -1
- morango/management/commands/cleanupsyncs.py +64 -14
- morango/migrations/0001_initial.py +0 -2
- morango/migrations/0001_squashed_0024_auto_20240129_1757.py +583 -0
- morango/migrations/0002_auto_20170511_0400.py +0 -2
- morango/migrations/0002_store_idx_morango_deserialize.py +21 -0
- morango/migrations/0003_auto_20170519_0543.py +0 -2
- morango/migrations/0004_auto_20170520_2112.py +0 -2
- morango/migrations/0005_auto_20170629_2139.py +0 -2
- morango/migrations/0006_instanceidmodel_system_id.py +0 -2
- morango/migrations/0007_auto_20171018_1615.py +0 -2
- morango/migrations/0008_auto_20171114_2217.py +0 -2
- morango/migrations/0009_auto_20171205_0252.py +0 -2
- morango/migrations/0010_auto_20171206_1615.py +0 -2
- morango/migrations/0011_sharedkey.py +0 -2
- morango/migrations/0012_auto_20180927_1658.py +0 -2
- morango/migrations/0013_auto_20190627_1513.py +0 -2
- morango/migrations/0014_syncsession_extra_fields.py +0 -2
- morango/migrations/0015_auto_20200508_2104.py +2 -3
- morango/migrations/0016_store_deserialization_error.py +2 -3
- morango/migrations/0017_store_last_transfer_session_id.py +1 -2
- morango/migrations/0018_auto_20210714_2216.py +2 -3
- morango/migrations/0019_auto_20220113_1807.py +2 -3
- morango/migrations/0020_postgres_fix_nullable.py +0 -2
- morango/migrations/0021_store_partition_index_create.py +0 -2
- morango/migrations/0022_rename_instance_fields.py +23 -0
- morango/migrations/0023_add_instance_id_fields.py +24 -0
- morango/migrations/0024_auto_20240129_1757.py +28 -0
- morango/models/__init__.py +0 -6
- morango/models/certificates.py +137 -28
- morango/models/core.py +36 -36
- morango/models/fields/crypto.py +20 -6
- morango/models/fields/uuids.py +2 -1
- morango/models/utils.py +5 -6
- morango/proquint.py +2 -3
- morango/registry.py +23 -47
- morango/sync/backends/utils.py +10 -11
- morango/sync/context.py +48 -40
- morango/sync/controller.py +10 -1
- morango/sync/operations.py +1 -1
- morango/sync/session.py +11 -0
- morango/sync/syncsession.py +41 -22
- morango/urls.py +3 -3
- morango/utils.py +2 -3
- {morango-0.6.14.dist-info → morango-0.8.6.dist-info}/METADATA +29 -14
- morango-0.8.6.dist-info/RECORD +79 -0
- {morango-0.6.14.dist-info → morango-0.8.6.dist-info}/WHEEL +1 -1
- morango/models/morango_mptt.py +0 -33
- morango/settings.py +0 -115
- morango/wsgi.py +0 -33
- morango-0.6.14.dist-info/RECORD +0 -77
- {morango-0.6.14.dist-info → morango-0.8.6.dist-info/licenses}/AUTHORS.md +0 -0
- {morango-0.6.14.dist-info → morango-0.8.6.dist-info/licenses}/LICENSE +0 -0
- {morango-0.6.14.dist-info → morango-0.8.6.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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
|
|
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 2021-07-14 22:16
|
|
3
|
-
from
|
|
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
|
|
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):
|
|
@@ -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
|
+
]
|
morango/models/__init__.py
CHANGED
|
@@ -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",
|
morango/models/certificates.py
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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
|
|
359
|
-
if
|
|
408
|
+
for partition in self:
|
|
409
|
+
if not other.contains_exact_partition(partition):
|
|
360
410
|
return False
|
|
361
|
-
for partition in other
|
|
362
|
-
if
|
|
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
|
-
|
|
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
|
-
#
|
|
244
|
-
|
|
245
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
354
|
+
isinstance(model, type) and issubclass(model, SyncableModel)
|
|
355
355
|
):
|
|
356
356
|
model = model.morango_model_name
|
|
357
|
-
_assert(isinstance(model,
|
|
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
|
-
#
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
klass_model.
|
|
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
|
|
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
|
|
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
|
|
845
|
-
if issubclass(model, SyncableModel)
|
|
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
|
-
|
|
932
|
-
|
|
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
|
-
|
|
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
|