Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
====================
Expand Down
3 changes: 2 additions & 1 deletion synchro/apps.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import absolute_import
from django.apps import AppConfig


Expand All @@ -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()
7 changes: 4 additions & 3 deletions synchro/core.py
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions synchro/handlers.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
63 changes: 47 additions & 16 deletions synchro/management/commands/synchronize.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import absolute_import
from datetime import datetime

from django import VERSION
Expand All @@ -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'):
Expand Down Expand Up @@ -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 = {}


Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {}
Expand All @@ -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.')
Expand Down
46 changes: 46 additions & 0 deletions synchro/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -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')},
},
),
]
Empty file added synchro/migrations/__init__.py
Empty file.
22 changes: 12 additions & 10 deletions synchro/models.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
7 changes: 6 additions & 1 deletion synchro/settings.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion synchro/signals.py
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
5 changes: 3 additions & 2 deletions synchro/templates/synchro.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ <h1>{% trans "Synchronization" %}</h1>
<form method="post">
{% csrf_token %}
<br>{% trans 'Last synchro time' %}: {{ last }}.
<br><br><input type="submit" name="synchro" value="{% trans 'Synchronize' %}">

<br><br><input type="submit" name="synchro" {% if not remote_db %}disabled{% endif %} value="{% trans 'Synchronize' %}">

{% if reset_allowed %}
<br><br><input type="submit" name="reset" value="{% trans 'Reset synchronization' %}"
onclick="return confirm('{% trans "All changes from last synchronization up to now will be forgotten and won't be synchronized in the future. Are you sure you want to proceed?" %}');">
onclick="return confirm(`All changes from last synchronization up to now will be forgotten and won't be synchronized in the future. Are you sure you want to proceed?`)">
{% endif %}
</form>
</div>
Expand Down
1 change: 1 addition & 0 deletions synchro/test_urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# flake8: noqa
from __future__ import absolute_import
from django.contrib import admin
from django.conf.urls import url, include

Expand Down
11 changes: 6 additions & 5 deletions synchro/tests.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import absolute_import
import datetime

from django import VERSION
Expand All @@ -16,10 +17,10 @@
except ImportError:
from django.utils.unittest.case import skipUnless

from models import ChangeLog
import settings as synchro_settings
from signals import DisableSynchroLog, disable_synchro_log
from utility import NaturalManager, reset_synchro, NaturalKeyModel
from .models import ChangeLog
from . import settings as synchro_settings
from .signals import DisableSynchroLog, disable_synchro_log
from .utility import NaturalManager, reset_synchro, NaturalKeyModel

from django.contrib.auth import get_user_model
User = get_user_model()
Expand Down Expand Up @@ -621,7 +622,7 @@ def test_foreign_keys(self):
b = PkModelWithSkip.objects.db_manager(REMOTE).get(name='James')
self.assertEqual(2, b.links.count())
# Check if all submodels belong to remote db
self.assertTrue(all(map(lambda x: x._state.db == REMOTE, b.links.all())))
self.assertTrue(all([x._state.db == REMOTE for x in b.links.all()]))

def test_disabling(self):
"""Test if logging can be disabled."""
Expand Down
3 changes: 2 additions & 1 deletion synchro/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# flake8: noqa
from __future__ import absolute_import
from django.conf.urls import url

from views import synchro
from .views import synchro


urlpatterns = (
Expand Down
Loading