From f2aa99ca81895b29d19d67f9804e7dbd8a2fa6f9 Mon Sep 17 00:00:00 2001 From: Emmanuel Cazenave Date: Wed, 17 Jan 2018 15:22:48 +0100 Subject: [PATCH] django 1.11 support * breaks backward compatibility : now nothing is importable directly from the root module (because django 1.11 requires almost empty root __init__.py for application) * create a migration file (django 1.11 start application friendliness) * adopt EO standard packaging practices (in packaging commands and version management) * adopt EO standard tests and CI practices (tox, pytest, jenkins.sh) --- django_journal/__init__.py | 75 ---------------------- django_journal/admin.py | 6 +- django_journal/main.py | 72 +++++++++++++++++++++ django_journal/middleware.py | 8 +-- django_journal/migrations/0001_initial.py | 103 ++++++++++++++++++++++++++++++ django_journal/migrations/__init__.py | 0 django_journal/models.py | 6 +- django_journal/tests.py | 20 ++++-- jenkins.sh | 4 ++ setup.py | 64 +++++++++---------- tox.ini | 9 +++ 11 files changed, 243 insertions(+), 124 deletions(-) create mode 100644 django_journal/main.py create mode 100644 django_journal/migrations/0001_initial.py create mode 100644 django_journal/migrations/__init__.py create mode 100755 jenkins.sh create mode 100644 tox.ini diff --git a/django_journal/__init__.py b/django_journal/__init__.py index 5859cbd..e69de29 100644 --- a/django_journal/__init__.py +++ b/django_journal/__init__.py @@ -1,75 +0,0 @@ -import logging - -from exceptions import JournalException -from models import (Journal, Tag, Template) - -import django.db.models -from django.conf import settings - -from decorator import atomic - -__all__ = ('record', 'error_record', 'Journal') -__version__ = '1.25.1' - -def unicode_truncate(s, length, encoding='utf-8'): - '''Truncate an unicode string so that its UTF-8 encoding is less than - length.''' - encoded = s.encode(encoding)[:length] - return encoded.decode(encoding, 'ignore') - -@atomic -def record(tag, template, using=None, **kwargs): - '''Record an event in the journal. The modification is done inside the - current transaction. - - tag: - a string identifier giving the type of the event - tpl: - a format string to describe the event - kwargs: - a mapping of object or data to interpolate in the format string - ''' - template = unicode(template) - tag = Tag.objects.using(using).get_cached(name=tag) - template = Template.objects.using(using).get_cached(content=template) - try: - message = template.content.format(**kwargs) - except (KeyError, IndexError), e: - raise JournalException( - 'Missing variable for the template message', template, e) - try: - logger = logging.getLogger('django.journal.%s' % tag) - if tag.name == 'error' or tag.name.startswith('error-'): - logger.error(message) - elif tag.name == 'warning' or tag.name.startswith('warning-'): - logger.warning(message) - else: - logger.info(message) - except: - try: - logging.getLogger('django.journal').exception('Unable to log msg') - except: - pass # we tried, really, we tried - journal = Journal.objects.using(using).create(tag=tag, template=template, - message=unicode_truncate(message, 128)) - for name, value in kwargs.iteritems(): - if value is None: - continue - tag = Tag.objects.using(using).get_cached(name=name) - if isinstance(value, django.db.models.Model): - journal.objectdata_set.create(tag=tag, content_object=value) - else: - journal.stringdata_set.create(tag=tag, content=unicode(value)) - return journal - -def error_record(tag, tpl, **kwargs): - '''Records error events. - - You must use this function when logging error events. It uses another - database alias than the default one to be immune to transaction rollback - when logging in the middle of a transaction which is going to - rollback. - ''' - return record(tag, tpl, - using=getattr(settings, 'JOURNAL_DB_FOR_ERROR_ALIAS', 'default'), - **kwargs) diff --git a/django_journal/admin.py b/django_journal/admin.py index faead5f..68e8dfd 100644 --- a/django_journal/admin.py +++ b/django_journal/admin.py @@ -4,14 +4,14 @@ import django.contrib.admin as admin from django.contrib.contenttypes.models import ContentType from django.utils.html import escape -from models import Journal, Tag, ObjectData, StringData - from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils.html import escape from django.core.urlresolvers import reverse, NoReverseMatch -import actions +from . import actions +from .models import Journal, Tag, ObjectData, StringData + class ModelAdminFormatter(Formatter): def __init__(self, model_admin=None, filter_link=True, diff --git a/django_journal/main.py b/django_journal/main.py new file mode 100644 index 0000000..1a43bb8 --- /dev/null +++ b/django_journal/main.py @@ -0,0 +1,72 @@ +import logging + +from django.conf import settings +import django.db.models + +from .decorator import atomic +from .exceptions import JournalException +from .models import (Journal, Tag, Template) + + +def unicode_truncate(s, length, encoding='utf-8'): + '''Truncate an unicode string so that its UTF-8 encoding is less than + length.''' + encoded = s.encode(encoding)[:length] + return encoded.decode(encoding, 'ignore') + +@atomic +def record(tag, template, using=None, **kwargs): + '''Record an event in the journal. The modification is done inside the + current transaction. + + tag: + a string identifier giving the type of the event + tpl: + a format string to describe the event + kwargs: + a mapping of object or data to interpolate in the format string + ''' + template = unicode(template) + tag = Tag.objects.using(using).get_cached(name=tag) + template = Template.objects.using(using).get_cached(content=template) + try: + message = template.content.format(**kwargs) + except (KeyError, IndexError), e: + raise JournalException( + 'Missing variable for the template message', template, e) + try: + logger = logging.getLogger('django.journal.%s' % tag) + if tag.name == 'error' or tag.name.startswith('error-'): + logger.error(message) + elif tag.name == 'warning' or tag.name.startswith('warning-'): + logger.warning(message) + else: + logger.info(message) + except: + try: + logging.getLogger('django.journal').exception('Unable to log msg') + except: + pass # we tried, really, we tried + journal = Journal.objects.using(using).create(tag=tag, template=template, + message=unicode_truncate(message, 128)) + for name, value in kwargs.iteritems(): + if value is None: + continue + tag = Tag.objects.using(using).get_cached(name=name) + if isinstance(value, django.db.models.Model): + journal.objectdata_set.create(tag=tag, content_object=value) + else: + journal.stringdata_set.create(tag=tag, content=unicode(value)) + return journal + +def error_record(tag, tpl, **kwargs): + '''Records error events. + + You must use this function when logging error events. It uses another + database alias than the default one to be immune to transaction rollback + when logging in the middle of a transaction which is going to + rollback. + ''' + return record(tag, tpl, + using=getattr(settings, 'JOURNAL_DB_FOR_ERROR_ALIAS', 'default'), + **kwargs) diff --git a/django_journal/middleware.py b/django_journal/middleware.py index 8b7ea51..ccedf2c 100644 --- a/django_journal/middleware.py +++ b/django_journal/middleware.py @@ -1,4 +1,4 @@ -import django_journal +from . import main class JournalMiddleware(object): '''Add record and error_record methods to the request object to log @@ -15,13 +15,13 @@ class JournalMiddleware(object): kwargs['user'] = user if 'ip' not in kwargs: kwargs['ip'] = ip - django_journal.record(tag, template, using=using,**kwargs) - def error_record(tag, template, using=None, **kwargs): + main.record(tag, template, using=using, **kwargs) + def error_record(tag, template, **kwargs): if 'user' not in kwargs: kwargs['user'] = user if 'ip' not in kwargs: kwargs['ip'] = ip - django_journal.error_record(tag, template, using=using, **kwargs) + main.error_record(tag, template, **kwargs) request.record = record request.error_record = error_record return None diff --git a/django_journal/migrations/0001_initial.py b/django_journal/migrations/0001_initial.py new file mode 100644 index 0000000..cec559d --- /dev/null +++ b/django_journal/migrations/0001_initial.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.9 on 2018-01-16 08:28 +from __future__ import unicode_literals + +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='Journal', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='time')), + ('message', models.CharField(db_index=True, max_length=128, verbose_name='message')), + ], + options={ + 'ordering': ('-id',), + 'verbose_name': 'journal entry', + 'verbose_name_plural': 'journal entries', + }, + ), + migrations.CreateModel( + name='ObjectData', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('object_id', models.PositiveIntegerField(db_index=True, verbose_name='object id')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', verbose_name='content type')), + ('journal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_journal.Journal', verbose_name='journal entry')), + ], + options={ + 'verbose_name': 'linked object', + }, + ), + migrations.CreateModel( + name='StringData', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.TextField(verbose_name='content')), + ('journal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_journal.Journal', verbose_name='journal entry')), + ], + options={ + 'verbose_name': 'linked text string', + }, + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(db_index=True, max_length=32, unique=True, verbose_name='name')), + ], + options={ + 'ordering': ('name',), + 'verbose_name': 'tag', + }, + ), + migrations.CreateModel( + name='Template', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', models.TextField(db_index=True, unique=True, verbose_name='content')), + ], + options={ + 'ordering': ('content',), + }, + ), + migrations.AddField( + model_name='stringdata', + name='tag', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_journal.Tag', verbose_name='tag'), + ), + migrations.AddField( + model_name='objectdata', + name='tag', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_journal.Tag', verbose_name='tag'), + ), + migrations.AddField( + model_name='journal', + name='tag', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='django_journal.Tag', verbose_name='tag'), + ), + migrations.AddField( + model_name='journal', + name='template', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='django_journal.Template', verbose_name='template'), + ), + migrations.AlterUniqueTogether( + name='stringdata', + unique_together=set([('journal', 'tag')]), + ), + migrations.AlterUniqueTogether( + name='objectdata', + unique_together=set([('journal', 'tag')]), + ), + ] diff --git a/django_journal/migrations/__init__.py b/django_journal/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_journal/models.py b/django_journal/models.py index c711722..30dd68b 100644 --- a/django_journal/models.py +++ b/django_journal/models.py @@ -1,10 +1,10 @@ import string from django.db import models -from django.contrib.contenttypes import generic +from django.contrib.contenttypes.fields import GenericForeignKey from django.utils.translation import ugettext_lazy as _ -import managers +from . import managers class Tag(models.Model): @@ -143,7 +143,7 @@ class ObjectData(models.Model): verbose_name=_('content type')) object_id = models.PositiveIntegerField(db_index=True, verbose_name=_('object id')) - content_object = generic.GenericForeignKey('content_type', + content_object = GenericForeignKey('content_type', 'object_id') class Meta: diff --git a/django_journal/tests.py b/django_journal/tests.py index 4fa4941..4520127 100644 --- a/django_journal/tests.py +++ b/django_journal/tests.py @@ -1,19 +1,17 @@ from django.test import TestCase from django.contrib.auth.models import User, Group -from django.db import transaction +from django.db.transaction import atomic - -from . import record +from .main import record from . import actions from . import models class JournalTestCase(TestCase): def setUp(self): - models.JOURNAL_METADATA_CACHE_TIMEOUT = 0 self.users = [] self.groups = [] - with transaction.commit_on_success(): + with atomic(): for i in range(20): self.users.append( User.objects.create(username='user%s' % i)) @@ -54,3 +52,15 @@ class JournalTestCase(TestCase): l = list(actions.export_as_csv_generator(qs)) self.assertEquals(l[1]['user'], '') + +def test_middleware(db): + + class FakeRequest(object): + META = dict() + + from .middleware import JournalMiddleware + jm = JournalMiddleware() + request = FakeRequest() + jm.process_request(request) + request.record('yes', 'we can') + request.error_record('me', 'too') diff --git a/jenkins.sh b/jenkins.sh new file mode 100755 index 0000000..163a4cc --- /dev/null +++ b/jenkins.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e + +pip install -U tox +tox -r diff --git a/setup.py b/setup.py index 4e2ade6..ee69651 100755 --- a/setup.py +++ b/setup.py @@ -1,11 +1,12 @@ #!/usr/bin/python import os +import subprocess import sys from setuptools import setup, find_packages from setuptools.command.install_lib import install_lib as _install_lib from distutils.command.build import build as _build -from setuptools.command.sdist import sdist as _sdist +from setuptools.command.sdist import sdist from distutils.cmd import Command @@ -59,8 +60,20 @@ class build(_build): sub_commands = [('compile_translations', None)] + _build.sub_commands -class sdist(_sdist): - sub_commands = [('compile_translations', None)] + _sdist.sub_commands +class eo_sdist(sdist): + + def run(self): + print "creating VERSION file" + if os.path.exists('VERSION'): + os.remove('VERSION') + version = get_version() + version_file = open('VERSION', 'w') + version_file.write(version) + version_file.close() + sdist.run(self) + print "removing VERSION file" + if os.path.exists('VERSION'): + os.remove('VERSION') class install_lib(_install_lib): @@ -70,36 +83,19 @@ class install_lib(_install_lib): def get_version(): - import glob - import re - import os - - version = None - for d in glob.glob('*'): - if not os.path.isdir(d): - continue - module_file = os.path.join(d, '__init__.py') - if not os.path.exists(module_file): - continue - for v in re.findall("""__version__ *= *['"](.*)['"]""", - open(module_file).read()): - assert version is None - version = v - if version: - break - assert version is not None + if os.path.exists('VERSION'): + version_file = open('VERSION', 'r') + version = version_file.read() + version_file.close() + return version if os.path.exists('.git'): - import subprocess - p = subprocess.Popen(['git','describe','--dirty', '--match=v*'], - stdout=subprocess.PIPE) + p = subprocess.Popen(['git', 'describe', '--match=v*'], stdout=subprocess.PIPE) result = p.communicate()[0] - assert p.returncode == 0, 'git returned non-zero' - new_version = result.split()[0][1:] - major_minor_release = new_version.split('-')[0] - assert version == major_minor_release, \ - '__version__ (%s) must match the last git annotated tag (%s)' % (version, major_minor_release) - version = new_version.replace('-', '.').replace('.g', '+g') - return version + version = result.split()[0][1:] + version = version.replace('-', '.') + return version + return '0' + setup(name='django-journal', version=get_version(), @@ -116,13 +112,13 @@ setup(name='django-journal', 'build': build, 'install_lib': install_lib, 'compile_translations': compile_translations, - 'sdist': sdist, + 'sdist': eo_sdist, 'test': test }, install_requires=[ - 'django >= 1.7', + 'django >= 1.11,<2.0', 'django-model-utils', ], setup_requires=[ - 'django >= 1.4.2', + 'django >= 1.11,<2.0', ]) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..494ad40 --- /dev/null +++ b/tox.ini @@ -0,0 +1,9 @@ +[testenv] +usedevelop = True +deps = + pytest + pytest-django +setenv = + DJANGO_SETTINGS_MODULE=test_settings +commands = + {posargs:py.test --junitxml=junit.xml django_journal/tests.py} -- 2.15.1