Skip to content
Open
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ Contibutors
Ivan Fedoseev
Dirk Eschler
Joanna Maryniak
Mlier
20 changes: 17 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ one for every project deployment and to perform synchronization between them.
Requirements
============

The app is tested to work with Django 1.7 - 1.11. If you want to use app in older versions of Django,
use the 0.6 release.
The app is tested to work with Django 2.0 - 2.2. If you want to use app in older versions of Django 1,
use the 0.7 release.

The app needs ``django-dbsettings`` to store the time of last synchronization.

Expand Down Expand Up @@ -92,7 +92,13 @@ In order to allow performing synchronization without shell access, you can use s

Include in your urls::

url(r'^synchro/', include('synchro.urls', 'synchro', 'synchro')),
from django.urls import path, include

urlpatterns = [
...
path('synchro/', include('synchro.urls')),
...
]

Then the view will be available at reversed url: ``synchro:synchro``.

Expand Down Expand Up @@ -384,6 +390,14 @@ Or raw way of manually changing synchro checkpoint::
Changelog
=========

**0.8.1** (23/01/2020)
- Support Django 2.0 - 2.2 and 3.0
- Support Python 3.5 - 3.8

**0.8** (02/12/2019)
- Support Django 2.0 - 2.2
- Dropped support for Django 1.8 - 1.11

**0.7** (12/11/2017)
- Support Django 1.8 - 1.11
- Dropped support for Django 1.6 and older
Expand Down
64 changes: 10 additions & 54 deletions runtests.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,15 @@
#!/usr/bin/env python
import os
import sys

import django
from django.conf import settings
from django.core.management import call_command


if not settings.configured:
settings.configure(
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
},
'remote_db': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
},
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sites',
'django.contrib.sessions',
'dbsettings',
'synchro',
),
SITE_ID = 1,
SYNCHRO_REMOTE = 'remote_db',
# ROOT_URLCONF ommited, because in Django 1.11 it need to be a valid module
USE_I18N = True,
MIDDLEWARE_CLASSES=(
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
),
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
],
},
},
],
)
from django.test.utils import get_runner

if django.VERSION >= (1, 7):
if __name__ == "__main__":
os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.test_settings'
django.setup()
call_command('test', 'synchro')
TestRunner = get_runner(settings)
test_runner = TestRunner() # debug : verbosity=2, keepdb=True
failures = test_runner.run_tests(["tests"])
sys.exit(bool(failures))
16 changes: 8 additions & 8 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
name='django-synchro',
description='Django app for database data synchronization.',
long_description=open('README.rst').read(),
version='0.7',
version='0.8.1',
author='Jacek Tomaszewski',
author_email='jacek.tomek@gmail.com',
url='https://github.com/zlorf/django-synchro',
license='MIT',
install_requires=(
'django-dbsettings>=0.7',
'django>=1.7',
'django-dbsettings>=0.11.0',
'django>=2.0',
),
classifiers=[
'Development Status :: 4 - Beta',
Expand All @@ -21,12 +21,12 @@
'Operating System :: OS Independent',
'Programming Language :: Python',
'Framework :: Django',
'Framework :: Django :: 1.7',
'Framework :: Django :: 1.8',
'Framework :: Django :: 1.9',
'Framework :: Django :: 1.10',
'Framework :: Django :: 1.11',
'Framework :: Django :: 2.0',
'Framework :: Django :: 2.1',
'Framework :: Django :: 2.2',
'Framework :: Django :: 3.0',
],
packages=find_packages(),
include_package_data = True,
zip_safe=False,
)
2 changes: 1 addition & 1 deletion synchro/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ class SynchroConfig(AppConfig):
verbose_name = 'Synchro'

def ready(self):
from signals import synchro_connect
from .signals import synchro_connect
synchro_connect()
6 changes: 3 additions & 3 deletions synchro/core.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from utility import NaturalManager, reset_synchro
from management.commands.synchronize import call_synchronize
from signals import DisableSynchroLog, disable_synchro_log
from .utility import NaturalManager, reset_synchro
from .management.commands.synchronize import call_synchronize
from .signals import DisableSynchroLog, disable_synchro_log
4 changes: 2 additions & 2 deletions synchro/handlers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import settings
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
25 changes: 14 additions & 11 deletions synchro/management/commands/synchronize.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
from datetime import datetime

from django import VERSION
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils.translation import ugettext_lazy as _t
from django.utils import timezone

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

import pytz


if not hasattr(transaction, 'atomic'):
# Django < 1.6 stub
Expand Down Expand Up @@ -71,11 +72,11 @@ def save_with_fks(ct, obj, new_pk):
old_id = obj.pk
obj._state.db = REMOTE

fks = (f for f in obj._meta.fields if f.rel)
fks = (f for f in obj._meta.fields if f.remote_field)
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)
fk_ct = ContentType.objects.get_for_model(f.remote_field.model)
rem, _ = ensure_exist(fk_ct, fk_id)
f.save_form_data(obj, rem)

Expand All @@ -100,7 +101,7 @@ def save_m2m(ct, obj, remote):
for f in obj._meta.many_to_many:
me = f.m2m_field_name()
he_id = '%s_id' % f.m2m_reverse_field_name()
res[f.attname] = (f.rel.to, f.rel.through, me, he_id)
res[f.attname] = (f.remote_field.model, f.remote_field.through, me, he_id)
if VERSION < (1, 8):
m2m = obj._meta.get_all_related_many_to_many_objects()
else:
Expand All @@ -114,13 +115,13 @@ def save_m2m(ct, obj, remote):
me = f.m2m_reverse_field_name()
he_id = '%s_id' % f.m2m_field_name()
related_model = rel.model if VERSION < (1, 8) else rel.related_model
res[rel.get_accessor_name()] = (related_model, f.rel.through, me, he_id)
res[rel.get_accessor_name()] = (related_model, f.remote_field.through, me, he_id)
M2M_CACHE[model_name] = res

_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 M2M_CACHE[model_name].items():
fk_ct = ContentType.objects.get_for_model(to)
out = []
if through._meta.auto_created:
Expand All @@ -135,9 +136,9 @@ 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 _m2m.items():
if not intermediary:
setattr(remote, f, out)
getattr(remote, f).set(out)
else:
getattr(remote, f).clear()
for inter in out:
Expand Down Expand Up @@ -187,7 +188,7 @@ 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.auto_field else 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,7 +262,8 @@ def synchronize(self, *args, **options):
raise exception_class('No REMOTE database specified in settings.')

since = app_options.last_check
last_time = datetime.now()
since = since.replace(tzinfo=pytz.utc)
last_time = timezone.now()
logs = ChangeLog.objects.filter(date__gt=since).select_related().order_by('date', 'pk')

# Don't synchronize if object should be added/changed and later deleted;
Expand All @@ -281,6 +283,7 @@ def synchronize(self, *args, **options):
ACTIONS[log.action](log.content_type, log.object_id, log)

if len(logs):
last_time = last_time.strftime("%Y-%m-%d %H:%M:%S.%f")
app_options.last_check = last_time
return _t('Synchronization performed successfully.')
else:
Expand Down
47 changes: 47 additions & 0 deletions synchro/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 2.1.10 on 2019-07-19 03:42

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.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.CharField(max_length=20)),
('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.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(max_length=200)),
('changelog', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='synchro.ChangeLog')),
],
),
migrations.CreateModel(
name='Reference',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('local_object_id', models.CharField(max_length=20)),
('remote_object_id', models.CharField(max_length=20)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
),
migrations.AlterUniqueTogether(
name='reference',
unique_together={('content_type', 'local_object_id')},
),
]
Empty file added synchro/migrations/__init__.py
Empty file.
8 changes: 5 additions & 3 deletions synchro/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@

class SynchroSettings(dbsettings.Group):
last_check = dbsettings.DateTimeValue('Last synchronization', default=now())


options = SynchroSettings()


class Reference(models.Model):
content_type = models.ForeignKey(ContentType)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
local_object_id = models.CharField(max_length=20)
remote_object_id = models.CharField(max_length=20)

Expand All @@ -32,7 +34,7 @@ class Meta:


class ChangeLog(models.Model):
content_type = models.ForeignKey(ContentType)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.CharField(max_length=20)
object = GenericForeignKey()
date = models.DateTimeField(auto_now=True)
Expand All @@ -43,5 +45,5 @@ def __unicode__(self):


class DeleteKey(models.Model):
changelog = models.OneToOneField(ChangeLog)
changelog = models.OneToOneField(ChangeLog, on_delete=models.CASCADE)
key = models.CharField(max_length=200)
4 changes: 2 additions & 2 deletions synchro/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ def _get_remote_field(m2m):
def get_intermediary(models):
res = {}
for model in models:
res.update((m2m.rel.through, _get_remote_field(m2m)) for m2m in model._meta.many_to_many
if not m2m.rel.through._meta.auto_created)
res.update((m2m.remote_field.through, _get_remote_field(m2m)) for m2m in model._meta.many_to_many
if not m2m.remote_field.through._meta.auto_created)
return res

MODELS = INTER_MODELS = []
Expand Down
2 changes: 1 addition & 1 deletion synchro/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


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/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# flake8: noqa
from django.conf.urls import url
from django.urls import path

from views import synchro
from .views import synchro


urlpatterns = (
url(r'^$', synchro, name='synchro'),
path('', synchro, name='synchro'),
)
Loading