diff --git a/README.rst b/README.rst index 240a913..a4998cb 100644 --- a/README.rst +++ b/README.rst @@ -122,6 +122,13 @@ Generally, ``SYNCHRO_REMOTE`` setting can behave in 3 different ways: __ synchro_on_remote_ +``SYNCHRO_CREATE_NEW`` setting +-------------------------- +SYNCHRO_CREATE_NEW should contain list of classes that should not be updated on remote server instead if they are created +on local database and the same primary key is taken on remote database, they will be created new instead of update. + +This feature is created keeping in mind the models that can not be changed but only created new + (i.e Logs) Remarks and features ==================== diff --git a/synchro/apps.py b/synchro/apps.py index 7073200..2cf4105 100644 --- a/synchro/apps.py +++ b/synchro/apps.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from django.apps import AppConfig @@ -6,5 +7,5 @@ class SynchroConfig(AppConfig): verbose_name = 'Synchro' def ready(self): - from signals import synchro_connect + from .signals import synchro_connect synchro_connect() diff --git a/synchro/core.py b/synchro/core.py index 8dcf883..8722b41 100644 --- a/synchro/core.py +++ b/synchro/core.py @@ -1,3 +1,4 @@ -from utility import NaturalManager, reset_synchro -from management.commands.synchronize import call_synchronize -from signals import DisableSynchroLog, disable_synchro_log +from __future__ import absolute_import +from .utility import NaturalManager, reset_synchro +from .management.commands.synchronize import call_synchronize +from .signals import DisableSynchroLog, disable_synchro_log diff --git a/synchro/handlers.py b/synchro/handlers.py index c524407..c790ad3 100644 --- a/synchro/handlers.py +++ b/synchro/handlers.py @@ -1,6 +1,7 @@ -import settings +from __future__ import absolute_import +from . import settings settings.prepare() -from models import ChangeLog, DeleteKey, ADDITION, CHANGE, DELETION, M2M_CHANGE +from .models import ChangeLog, DeleteKey, ADDITION, CHANGE, DELETION, M2M_CHANGE def delete_redundant_change(cl): diff --git a/synchro/management/commands/synchronize.py b/synchro/management/commands/synchronize.py index 6628b2d..6b1009d 100644 --- a/synchro/management/commands/synchronize.py +++ b/synchro/management/commands/synchronize.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from datetime import datetime from django import VERSION @@ -9,7 +10,8 @@ from synchro.models import Reference, ChangeLog, DeleteKey, options as app_options from synchro.models import ADDITION, CHANGE, DELETION, M2M_CHANGE -from synchro.settings import REMOTE, LOCAL +from synchro.settings import REMOTE, LOCAL, SYNCHRO_CREATE_NEW +import six if not hasattr(transaction, 'atomic'): @@ -70,23 +72,46 @@ def save_with_fks(ct, obj, new_pk): """ old_id = obj.pk obj._state.db = REMOTE + new_pk_list = SYNCHRO_CREATE_NEW # Add models here that should not have the same primary key + rem = find_natural(ct, obj) + skip = False + if rem is not None: + if is_remote_newer(obj, rem): + skip = True + + new_obj= obj.__class__.objects.filter(pk=obj.pk).using(REMOTE) - fks = (f for f in obj._meta.fields if f.rel) - for f in fks: - fk_id = f.value_from_object(obj) - if fk_id is not None: - fk_ct = ContentType.objects.get_for_model(f.rel.to) - rem, _ = ensure_exist(fk_ct, fk_id) - f.save_form_data(obj, rem) + if new_obj.exists() and type(obj) not in new_pk_list: + if not skip: + new_obj = new_obj[0] + values = obj.__dict__ + values.pop('_state') + if '_django_cleanup_original_cache' in values.keys(): + values.pop('_django_cleanup_original_cache') + obj.__class__.objects.filter(pk=obj.pk).using(REMOTE).update(**values) + + else: + fks = (f for f in obj._meta.fields if (f.many_to_one or f.one_to_one)) + for f in fks: + fk_id = f.value_from_object(obj) + if fk_id is not None: + fk_ct = ContentType.objects.get_for_model(f.related_model()) + rem, _ = ensure_exist(fk_ct, fk_id) + f.save_form_data(obj, rem) + + if not new_obj and type(obj) not in new_pk_list: + obj.pk = new_pk + else: + obj.pk = None + obj.save(using=REMOTE) - obj.pk = new_pk - obj.save(using=REMOTE) r, n = Reference.objects.get_or_create(content_type=ct, local_object_id=old_id, defaults={'remote_object_id': obj.pk}) if not n and r.remote_object_id != obj.pk: r.remote_object_id = obj.pk r.save() + M2M_CACHE = {} @@ -120,7 +145,7 @@ def save_m2m(ct, obj, remote): _m2m = {} # handle m2m fields - for f, (to, through, me, he_id) in M2M_CACHE[model_name].iteritems(): + for f, (to, through, me, he_id) in six.iteritems(M2M_CACHE[model_name]): fk_ct = ContentType.objects.get_for_model(to) out = [] if through._meta.auto_created: @@ -135,7 +160,7 @@ def save_m2m(ct, obj, remote): out.append(inter) _m2m[f] = not through._meta.auto_created, out - for f, (intermediary, out) in _m2m.iteritems(): + for f, (intermediary, out) in six.iteritems(_m2m): if not intermediary: setattr(remote, f, out) else: @@ -187,7 +212,8 @@ def perform_add(ct, id, log=None): change_with_fks(ct, obj, rem) rem = obj else: - new_pk = None if obj._meta.has_auto_field else obj.pk + # new_pk = None if obj._meta.has_auto_field else obj.pk + new_pk = obj.pk create_with_fks(ct, obj, new_pk) rem = obj ref, _ = Reference.objects.get_or_create(content_type=ct, local_object_id=id, @@ -261,8 +287,13 @@ def synchronize(self, *args, **options): raise exception_class('No REMOTE database specified in settings.') since = app_options.last_check - last_time = datetime.now() - logs = ChangeLog.objects.filter(date__gt=since).select_related().order_by('date', 'pk') + from django.utils import timezone + + last_time = timezone.now() + check_time = last_time + + filters = {"date__gt": since} if since else {} + logs = ChangeLog.objects.filter(**filters).select_related().order_by('date', 'pk') # Don't synchronize if object should be added/changed and later deleted; to_del = {} @@ -281,7 +312,7 @@ def synchronize(self, *args, **options): ACTIONS[log.action](log.content_type, log.object_id, log) if len(logs): - app_options.last_check = last_time + app_options.last_check = check_time return _t('Synchronization performed successfully.') else: return _t('No changes since last synchronization.') diff --git a/synchro/migrations/0001_initial.py b/synchro/migrations/0001_initial.py new file mode 100644 index 0000000..df45c60 --- /dev/null +++ b/synchro/migrations/0001_initial.py @@ -0,0 +1,46 @@ +# Generated by Django 3.2.5 on 2022-06-20 11:58 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='ChangeLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.CharField(max_length=256)), + ('date', models.DateTimeField(auto_now=True)), + ('action', models.PositiveSmallIntegerField(choices=[(1, 'Add'), (2, 'Change'), (3, 'Delete'), (4, 'M2m Change')])), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ], + ), + migrations.CreateModel( + name='DeleteKey', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(max_length=256)), + ('changelog', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='synchro.changelog')), + ], + ), + migrations.CreateModel( + name='Reference', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('local_object_id', models.CharField(max_length=256)), + ('remote_object_id', models.CharField(max_length=256)), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ], + options={ + 'unique_together': {('content_type', 'local_object_id')}, + }, + ), + ] diff --git a/synchro/migrations/__init__.py b/synchro/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/synchro/models.py b/synchro/models.py index 6a31b34..596d7fe 100644 --- a/synchro/models.py +++ b/synchro/models.py @@ -1,10 +1,12 @@ +from __future__ import absolute_import import django from django.contrib.admin.models import ADDITION, CHANGE, DELETION from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey from django.db import models -from django.utils.timezone import now +from django.utils import timezone import dbsettings +import six M2M_CHANGE = 4 @@ -18,30 +20,30 @@ class SynchroSettings(dbsettings.Group): - last_check = dbsettings.DateTimeValue('Last synchronization', default=now()) + last_check = dbsettings.DateTimeValue('Last synchronization', default=timezone.now) options = SynchroSettings() class Reference(models.Model): - content_type = models.ForeignKey(ContentType) - local_object_id = models.CharField(max_length=20) - remote_object_id = models.CharField(max_length=20) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + local_object_id = models.CharField(max_length=256) + remote_object_id = models.CharField(max_length=256) class Meta: unique_together = ('content_type', 'local_object_id') class ChangeLog(models.Model): - content_type = models.ForeignKey(ContentType) - object_id = models.CharField(max_length=20) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.CharField(max_length=256) object = GenericForeignKey() date = models.DateTimeField(auto_now=True) action = models.PositiveSmallIntegerField(choices=ACTIONS) def __unicode__(self): - return u'ChangeLog for %s (%s)' % (unicode(self.object), self.get_action_display()) + return u'ChangeLog for %s (%s)' % (six.text_type(self.object), self.get_action_display()) class DeleteKey(models.Model): - changelog = models.OneToOneField(ChangeLog) - key = models.CharField(max_length=200) + changelog = models.OneToOneField(ChangeLog, on_delete=models.CASCADE) + key = models.CharField(max_length=256) diff --git a/synchro/settings.py b/synchro/settings.py index 59a896c..3166c7b 100644 --- a/synchro/settings.py +++ b/synchro/settings.py @@ -1,6 +1,8 @@ +from __future__ import absolute_import from django.apps import apps from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from six.moves import map def get_all_models(app): @@ -21,7 +23,7 @@ def parse(model): raise ImproperlyConfigured( 'SYNCHRO_MODELS: Model %s not found in %s app.' % (model, app)) return m - return map(parse, l) + return list(map(parse, l)) def parse_models(l): @@ -66,6 +68,9 @@ def prepare(): ALLOW_RESET = getattr(settings, 'SYNCHRO_ALLOW_RESET', True) DEBUG = getattr(settings, 'SYNCHRO_DEBUG', False) +# list of classes that will be created new while sync i.e Logs class +SYNCHRO_CREATE_NEW = getattr(settings, 'SYNCHRO_CREATE_NEW', []) + if REMOTE is None: if not hasattr(settings, 'SYNCHRO_REMOTE'): import warnings diff --git a/synchro/signals.py b/synchro/signals.py index de1618c..7897bb4 100644 --- a/synchro/signals.py +++ b/synchro/signals.py @@ -1,10 +1,11 @@ +from __future__ import absolute_import from functools import wraps from django.db.models.signals import post_save, post_delete, m2m_changed def synchro_connect(): - from handlers import save_changelog_add_chg, save_changelog_del, save_changelog_m2m + from .handlers import save_changelog_add_chg, save_changelog_del, save_changelog_m2m post_save.connect(save_changelog_add_chg, dispatch_uid='synchro_add_chg') post_delete.connect(save_changelog_del, dispatch_uid='synchro_del') m2m_changed.connect(save_changelog_m2m, dispatch_uid='synchro_m2m') diff --git a/synchro/templates/synchro.html b/synchro/templates/synchro.html index bbfe0d4..0c65bab 100644 --- a/synchro/templates/synchro.html +++ b/synchro/templates/synchro.html @@ -18,11 +18,12 @@