misc: apply black/isort/pyupgrade (#54260)

This commit is contained in:
Paul Marillonnet 2021-07-15 12:11:27 +02:00
parent 0855ad22c3
commit 7233202758
11 changed files with 316 additions and 254 deletions

View File

@ -1,13 +1,12 @@
import csv
from django.utils.translation import ugettext_lazy as _
from django.http import HttpResponse
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from . import models
def export_as_csv_generator(queryset):
header = ['time', 'tag', 'message']
tags = set(models.Tag.objects.filter(objectdata__journal__in=queryset).values_list('name', flat=True))
@ -15,13 +14,13 @@ def export_as_csv_generator(queryset):
tags.add('%s__id' % tag)
tags |= set(models.Tag.objects.filter(stringdata__journal__in=queryset).values_list('name', flat=True))
extra_headers = list(sorted(tags))
yield header+extra_headers
yield header + extra_headers
for journal in queryset:
row = {
'time': journal.time.isoformat(' '),
'tag': force_text(journal.tag.name),
'message': force_text(journal),
}
'time': journal.time.isoformat(' '),
'tag': force_text(journal.tag.name),
'message': force_text(journal),
}
for stringdata in journal.stringdata_set.all():
row_name = stringdata.tag.name.encode('utf-8')
row[force_text(row_name)] = force_text(stringdata.content)
@ -34,6 +33,7 @@ def export_as_csv_generator(queryset):
row[row_name] = force_text(objectdata.content_object)
yield row
def export_as_csv(modeladmin, request, queryset):
"""
CSV export for journal
@ -47,4 +47,6 @@ def export_as_csv(modeladmin, request, queryset):
for row in l:
writer.writerow(row)
return response
export_as_csv.short_description = _(u"Export CSV file")
export_as_csv.short_description = _("Export CSV file")

View File

@ -2,47 +2,48 @@ from string import Formatter
import django.contrib.admin as admin
from django.contrib.contenttypes.models import ContentType
from django.utils.html import format_html, escape, mark_safe
from django.db import models
from django.urls import NoReverseMatch, reverse
from django.utils.encoding import force_text
from django.utils.html import escape, format_html, mark_safe
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse, NoReverseMatch
from .models import Journal, Tag, ObjectData, StringData
from .actions import export_as_csv
from .models import Journal, ObjectData, StringData, Tag
class ModelAdminFormatter(Formatter):
def __init__(self, model_admin=None, filter_link=True,
object_link=True):
def __init__(self, model_admin=None, filter_link=True, object_link=True):
self.filter_link = filter_link
self.object_link = object_link
self.model_admin = model_admin
super(ModelAdminFormatter, self).__init__()
super().__init__()
def build_object_link(self, value):
content_type = ContentType.objects.get_for_model(value.__class__)
url = u'{0}:{1}_{2}_change'.format(self.model_admin.admin_site.name,
content_type.app_label, content_type.model)
url = '{}:{}_{}_change'.format(
self.model_admin.admin_site.name, content_type.app_label, content_type.model
)
try:
url = reverse(url, args=(value.pk,))
except NoReverseMatch:
return u''
return u'<a href="{0}" class="external-link"></a>'.format(escape(url))
return ''
return f'<a href="{escape(url)}" class="external-link"></a>'
def format_field(self, value, format_spec):
if isinstance(value, models.Model):
res = ''
if self.filter_link:
content_type = ContentType.objects.get_for_model(value.__class__)
res = u'<a href="?objectdata__content_type={0}&objectdata__object_id={1}">{2}</a>'.format(
content_type.id, value.pk, escape(force_text(value)))
res = '<a href="?objectdata__content_type={}&objectdata__object_id={}">{}</a>'.format(
content_type.id, value.pk, escape(force_text(value))
)
else:
res = escape(force_text(value))
if self.object_link:
res += self.build_object_link(value)
return res
return escape(super(ModelAdminFormatter, self).format_field(value, format_spec))
return escape(super().format_field(value, format_spec))
class ObjectDataInlineAdmin(admin.TabularInline):
@ -52,6 +53,7 @@ class ObjectDataInlineAdmin(admin.TabularInline):
extra = 0
max_num = 0
class StringDataInlineAdmin(admin.TabularInline):
model = StringData
fields = ('tag', 'content')
@ -59,91 +61,98 @@ class StringDataInlineAdmin(admin.TabularInline):
extra = 0
max_num = 0
class JournalAdmin(admin.ModelAdmin):
list_display = ('time', '_tag', 'user', 'ip', 'message_for_list')
list_filter = ('tag',)
fields = ('time', 'tag', 'user', 'ip', 'message_for_change')
readonly_fields = fields
inlines = (
ObjectDataInlineAdmin,
StringDataInlineAdmin,
ObjectDataInlineAdmin,
StringDataInlineAdmin,
)
date_hierarchy = 'time'
search_fields = ('message','tag__name','time')
actions = [ export_as_csv ]
search_fields = ('message', 'tag__name', 'time')
actions = [export_as_csv]
class Media:
css = {
'all': ('journal/css/journal.css',),
'all': ('journal/css/journal.css',),
}
def queryset(self, request):
'''Get as much data as possible using the fewest requests possible.'''
qs = super(JournalAdmin, self).queryset(request)
qs = qs.select_related('tag', 'template') \
.prefetch_related('objectdata_set__content_type',
'stringdata_set', 'objectdata_set__tag',
'stringdata_set__tag', 'objectdata_set__content_object')
qs = super().queryset(request)
qs = qs.select_related('tag', 'template').prefetch_related(
'objectdata_set__content_type',
'stringdata_set',
'objectdata_set__tag',
'stringdata_set__tag',
'objectdata_set__content_object',
)
return qs
def lookup_allowed(self, key, *args, **kwargs):
return True
def _tag(self, entry):
name = entry.tag.name.replace(u'-', u'\u2011')
res = format_html('<a href="?tag__id__exact={0}">{1}</a>',
escape(entry.tag.id), escape(name))
name = entry.tag.name.replace('-', '\u2011')
res = format_html('<a href="?tag__id__exact={0}">{1}</a>', escape(entry.tag.id), escape(name))
return res
_tag.short_description = _('tag')
def ip(self, entry):
'''Search and return any associated stringdata whose tag is "ip"'''
for stringdata in entry.stringdata_set.all():
if stringdata.tag.name == 'ip':
return format_html('<a href="?stringdata__tag__id={tag_id}&' \
'stringdata__content={ip}">{ip}</a>',
tag_id=stringdata.tag.id, ip=stringdata.content)
return format_html(
'<a href="?stringdata__tag__id={tag_id}&' 'stringdata__content={ip}">{ip}</a>',
tag_id=stringdata.tag.id,
ip=stringdata.content,
)
return _('None')
ip.short_description = _('IP')
def user(self, entry):
'''Search and return any associated objectdata whose tag is "user"'''
for objectdata in entry.objectdata_set.all():
if objectdata.tag.name == 'user':
return format_html(self.object_filter_link(objectdata) + \
self.object_link(objectdata))
return format_html(self.object_filter_link(objectdata) + self.object_link(objectdata))
return _('None')
user.short_description = _('User')
def object_filter_link(self, objectdata):
if objectdata.content_object is not None:
caption = force_text(objectdata.content_object)
else:
caption = _(u'<deleted {content_type} {object_id}>').format(
content_type=objectdata.content_type,
object_id=objectdata.object_id)
return u'<a href="?objectdata__content_type={0}&objectdata__object_id={1}">{2}</a>'.format(
objectdata.content_type_id,
objectdata.object_id,
escape(caption))
caption = _('<deleted {content_type} {object_id}>').format(
content_type=objectdata.content_type, object_id=objectdata.object_id
)
return '<a href="?objectdata__content_type={}&objectdata__object_id={}">{}</a>'.format(
objectdata.content_type_id, objectdata.object_id, escape(caption)
)
def object_link(self, obj_data):
if obj_data.content_object is None:
return u''
url = u'{0}:{1}_{2}_change'.format(self.admin_site.name,
obj_data.content_type.app_label,
obj_data.content_type.model)
return ''
url = '{}:{}_{}_change'.format(
self.admin_site.name, obj_data.content_type.app_label, obj_data.content_type.model
)
try:
url = reverse(url, args=(obj_data.object_id,))
except NoReverseMatch:
return ''
return u'<a href="{0}" class="external-link"></a>'.format(url)
return f'<a href="{url}" class="external-link"></a>'
def message_for_change(self, entry):
ctx = entry.message_context()
formatter = ModelAdminFormatter(model_admin=self, filter_link=False)
message = formatter.format(escape(entry.template.content), **ctx)
return format_html('<span>{}</span>', mark_safe(message))
message_for_change.short_description = _('Message')
def message_for_list(self, entry):
@ -151,8 +160,10 @@ class JournalAdmin(admin.ModelAdmin):
formatter = ModelAdminFormatter(model_admin=self)
message = formatter.format(entry.template.content, **ctx)
return format_html('<span>{}</span>', mark_safe(message))
message_for_list.short_description = _('Message')
message_for_list.admin_order_field = 'message'
admin.site.register(Journal, JournalAdmin)
admin.site.register(Tag)

View File

@ -1,10 +1,12 @@
from functools import wraps
from django.db import transaction, DEFAULT_DB_ALIAS
from django.db import DEFAULT_DB_ALIAS, transaction
if hasattr(transaction, 'atomic'):
atomic = transaction.atomic
else:
class Transaction(object):
class Transaction:
sid = None
def __init__(self, using=None):
@ -41,8 +43,8 @@ else:
def wrapper(*args, **kwargs):
with self.__class__(using=self.using):
return func(*args, **kwargs)
return wrapper
return wrapper
def atomic(using=None):
"""
@ -56,5 +58,3 @@ else:
if callable(using):
return Transaction(DEFAULT_DB_ALIAS)(using)
return Transaction(using)

View File

@ -1,8 +1,8 @@
import logging
import django.db.models
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
import django.db.models
from django.utils.encoding import force_text
from .decorator import atomic
@ -11,32 +11,31 @@ 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.'''
"""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.
"""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
'''
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 = force_text(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) as e:
raise JournalException(
'Missing variable for the template message', template, 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-'):
@ -49,17 +48,19 @@ def record(tag, template, using=None, **kwargs):
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))
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.items():
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_type=ContentType.objects.db_manager(using).get_for_model(value),
object_id=value.pk
tag=tag,
content_type=ContentType.objects.db_manager(using).get_for_model(value),
object_id=value.pk,
)
else:
journal.stringdata_set.create(tag=tag, content=force_text(value))
@ -67,13 +68,13 @@ def record(tag, template, using=None, **kwargs):
def error_record(tag, tpl, **kwargs):
'''Records error events.
"""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.
'''
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.
"""
if kwargs.get('using') is None:
kwargs['using'] = getattr(settings, 'JOURNAL_DB_FOR_ERROR_ALIAS', 'default')

View File

@ -1,6 +1,6 @@
from django.contrib.contenttypes.models import ContentType
from django.db.models import Manager, Q
from django.db.models.query import QuerySet
from django.db.models import Q, Manager
class CachedQuerySet(QuerySet):
@ -27,34 +27,29 @@ class JournalQuerySet(QuerySet):
'''Return Journal records linked to this object.'''
content_type = ContentType.objects.get_for_model(obj)
if tag is None:
return self.filter(objectdata__content_type=content_type,
objectdata__object_id=obj.pk)
return self.filter(objectdata__content_type=content_type, objectdata__object_id=obj.pk)
else:
return self.filter(
objectdata__tag__name=tag,
objectdata__content_type=content_type,
objectdata__object_id=obj.pk)
objectdata__tag__name=tag, objectdata__content_type=content_type, objectdata__object_id=obj.pk
)
def for_objects(self, objects):
'''Return journal records linked to any of this objects.
"""Return journal records linked to any of this objects.
All objects must have the same model.
'''
All objects must have the same model.
"""
if not objects:
return self.none()
content_types = [ ContentType.objects.get_for_model(obj)
for obj in objects ]
content_types = [ContentType.objects.get_for_model(obj) for obj in objects]
if len(set(content_types)) != 1:
raise ValueError('objects must have of the same content type')
pks = [ obj.pk for obj in objects ]
return self.filter(
objectdata__content_type=content_types[0],
objectdata__object_id__in=pks)
pks = [obj.pk for obj in objects]
return self.filter(objectdata__content_type=content_types[0], objectdata__object_id__in=pks)
def for_tag(self, tag):
'''Returns Journal records linked to this tag by their own tag or
the tag on their data records.
'''
"""Returns Journal records linked to this tag by their own tag or
the tag on their data records.
"""
from . import models
if not isinstance(tag, models.Tag):
@ -64,17 +59,22 @@ class JournalQuerySet(QuerySet):
return self.none()
# always remember: multiple join (OR in WHERE) produces duplicate
# lines ! Use .distinct() for safety.
return self.filter(Q(tag=tag)|
Q(objectdata__tag=tag)|
Q(stringdata__tag=tag)) \
.distinct()
return self.filter(Q(tag=tag) | Q(objectdata__tag=tag) | Q(stringdata__tag=tag)).distinct()
class JournalManager(Manager.from_queryset(JournalQuerySet)):
def get_query_set(self):
return super(JournalManager, self).get_query_set() \
.prefetch_related('objectdata_set__content_type',
'stringdata_set', 'objectdata_set__tag',
'stringdata_set__tag', 'objectdata_set__content_object',
'tag', 'template') \
.select_related('tag', 'template')
return (
super()
.get_query_set()
.prefetch_related(
'objectdata_set__content_type',
'stringdata_set',
'objectdata_set__tag',
'stringdata_set__tag',
'objectdata_set__content_object',
'tag',
'template',
)
.select_related('tag', 'template')
)

View File

@ -1,29 +1,33 @@
from django.utils.deprecation import MiddlewareMixin
from django_journal import journal
class JournalMiddleware(MiddlewareMixin):
'''Add record and error_record methods to the request object to log
current user and current REMOTE_ADRESS.
"""Add record and error_record methods to the request object to log
current user and current REMOTE_ADRESS.
It must be setup after the auth middleware.
'''
It must be setup after the auth middleware.
"""
def process_request(self, request):
user = getattr(request, 'user', None)
ip = request.META.get('REMOTE_ADDR', None)
def record(tag, template, using=None, **kwargs):
if 'user' not in kwargs:
kwargs['user'] = user
if 'ip' not in kwargs:
kwargs['ip'] = ip
journal.record(tag, template, using=using,**kwargs)
journal.record(tag, template, using=using, **kwargs)
def error_record(tag, template, using=None, **kwargs):
if 'user' not in kwargs:
kwargs['user'] = user
if 'ip' not in kwargs:
kwargs['ip'] = ip
journal.error_record(tag, template, using=using, **kwargs)
request.record = record
request.error_record = error_record
return None

View File

@ -1,8 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
@ -15,7 +12,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Journal',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('time', models.DateTimeField(auto_now_add=True, verbose_name='time', db_index=True)),
('message', models.CharField(max_length=128, verbose_name='message', db_index=True)),
],
@ -28,10 +28,23 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='ObjectData',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('object_id', models.PositiveIntegerField(verbose_name='object id', db_index=True)),
('content_type', models.ForeignKey(verbose_name='content type', to='contenttypes.ContentType', on_delete=models.CASCADE)),
('journal', models.ForeignKey(verbose_name='journal entry', to='django_journal.Journal', on_delete=models.CASCADE)),
(
'content_type',
models.ForeignKey(
verbose_name='content type', to='contenttypes.ContentType', on_delete=models.CASCADE
),
),
(
'journal',
models.ForeignKey(
verbose_name='journal entry', to='django_journal.Journal', on_delete=models.CASCADE
),
),
],
options={
'verbose_name': 'linked object',
@ -40,9 +53,17 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='StringData',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('content', models.TextField(verbose_name='content')),
('journal', models.ForeignKey(verbose_name='journal entry', to='django_journal.Journal', on_delete=models.CASCADE)),
(
'journal',
models.ForeignKey(
verbose_name='journal entry', to='django_journal.Journal', on_delete=models.CASCADE
),
),
],
options={
'verbose_name': 'linked text string',
@ -51,7 +72,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('name', models.CharField(unique=True, max_length=32, verbose_name='name', db_index=True)),
],
options={
@ -62,7 +86,10 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Template',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
(
'id',
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
),
('content', models.TextField(unique=True, verbose_name='content', db_index=True)),
],
options={
@ -82,19 +109,25 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='journal',
name='tag',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, verbose_name='tag', to='django_journal.Tag'),
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, verbose_name='tag', to='django_journal.Tag'
),
),
migrations.AddField(
model_name='journal',
name='template',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, verbose_name='template', to='django_journal.Template'),
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
verbose_name='template',
to='django_journal.Template',
),
),
migrations.AlterUniqueTogether(
name='stringdata',
unique_together=set([('journal', 'tag')]),
unique_together={('journal', 'tag')},
),
migrations.AlterUniqueTogether(
name='objectdata',
unique_together=set([('journal', 'tag')]),
unique_together={('journal', 'tag')},
),
]

View File

@ -1,7 +1,7 @@
import string
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
@ -10,14 +10,14 @@ from . import managers
@python_2_unicode_compatible
class Tag(models.Model):
'''Tag allows typing event and data linked to events.
"""Tag allows typing event and data linked to events.
name:
the string identifier of the tag
"""
name:
the string identifier of the tag
'''
objects = managers.TagManager()
name = models.CharField(verbose_name=_('name'), max_length=32, unique=True,
db_index=True)
name = models.CharField(verbose_name=_('name'), max_length=32, unique=True, db_index=True)
def __str__(self):
return self.name
@ -32,14 +32,14 @@ class Tag(models.Model):
@python_2_unicode_compatible
class Template(models.Model):
'''Template for formatting an event.
"""Template for formatting an event.
ex.: Template(
content='{user1} gave group {group} to {user2}')
"""
ex.: Template(
content='{user1} gave group {group} to {user2}')
'''
objects = managers.TemplateManager()
content = models.TextField(verbose_name=_('content'), unique=True,
db_index=True)
content = models.TextField(verbose_name=_('content'), unique=True, db_index=True)
def __str__(self):
return self.content
@ -53,26 +53,23 @@ class Template(models.Model):
@python_2_unicode_compatible
class Journal(models.Model):
'''One line of the journal.
"""One line of the journal.
Each recorded event in the journal is a Journal instance.
Each recorded event in the journal is a Journal instance.
time - the time at which the event was recorded
tag - the tag giving the type of event
template - a format string to present the event
message - a simple string representation of the event, computed using
the template and associated datas.
"""
time - the time at which the event was recorded
tag - the tag giving the type of event
template - a format string to present the event
message - a simple string representation of the event, computed using
the template and associated datas.
'''
objects = managers.JournalManager()
time = models.DateTimeField(verbose_name=_('time'), auto_now_add=True,
db_index=True)
tag = models.ForeignKey(Tag, verbose_name=_('tag'),
on_delete=models.PROTECT)
template = models.ForeignKey(Template, verbose_name=_('template'),
on_delete=models.PROTECT)
message = models.CharField(verbose_name=_('message'), max_length=128,
db_index=True)
time = models.DateTimeField(verbose_name=_('time'), auto_now_add=True, db_index=True)
tag = models.ForeignKey(Tag, verbose_name=_('tag'), on_delete=models.PROTECT)
template = models.ForeignKey(Template, verbose_name=_('template'), on_delete=models.PROTECT)
message = models.CharField(verbose_name=_('message'), max_length=128, db_index=True)
class Meta:
ordering = ('-id',)
@ -85,8 +82,9 @@ class Journal(models.Model):
if data.content_object is not None:
ctx[data.tag.name] = data.content_object
else:
ctx[data.tag.name] = u'<deleted {content_type} {object_id}>'.format(
content_type=data.content_type, object_id=data.object_id)
ctx[data.tag.name] = '<deleted {content_type} {object_id}>'.format(
content_type=data.content_type, object_id=data.object_id
)
for data in self.stringdata_set.all():
ctx[data.tag.name] = data.content
for text, field, format_spec, conversion in string.Formatter().parse(self.template.content):
@ -98,30 +96,29 @@ class Journal(models.Model):
return ctx
def add_object_tag(self, tag_name, obj):
ObjectData(journal=self,
tag=Tag.objects.get_cached(name=tag_name),
content_object=obj).save()
ObjectData(journal=self, tag=Tag.objects.get_cached(name=tag_name), content_object=obj).save()
def __str__(self):
ctx = self.message_context()
return self.template.content.format(**ctx)
def __repr__(self):
return '<Journal pk:{0} tag:{1} message:{2}>'.format(
self.pk, unicode(self.tag).encode('utf-8'),
unicode(self.message).encode('utf-8'))
return '<Journal pk:{} tag:{} message:{}>'.format(
self.pk, unicode(self.tag).encode('utf-8'), unicode(self.message).encode('utf-8')
)
class StringData(models.Model):
'''String data associated to a recorded event.
"""String data associated to a recorded event.
journal:
the recorded event
tag:
the identifier for this data
content:
the string value of the data
"""
journal:
the recorded event
tag:
the identifier for this data
content:
the string value of the data
'''
journal = models.ForeignKey(Journal, verbose_name=_('journal entry'), on_delete=models.CASCADE)
tag = models.ForeignKey(Tag, verbose_name=_('tag'), on_delete=models.CASCADE)
content = models.TextField(verbose_name=_('content'))
@ -133,27 +130,27 @@ class StringData(models.Model):
@python_2_unicode_compatible
class ObjectData(models.Model):
'''Object data associated with a recorded event.
"""Object data associated with a recorded event.
journal:
the recorded event
tag:
the identifier for this data
content_object:
the object value of the data
"""
journal:
the recorded event
tag:
the identifier for this data
content_object:
the object value of the data
'''
journal = models.ForeignKey(Journal, verbose_name=_('journal entry'), on_delete=models.CASCADE)
tag = models.ForeignKey(Tag, verbose_name=_('tag'), on_delete=models.CASCADE)
content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE,
verbose_name=_('content type'))
object_id = models.PositiveIntegerField(db_index=True,
verbose_name=_('object id'))
content_object = GenericForeignKey('content_type',
'object_id')
content_type = models.ForeignKey(
'contenttypes.ContentType', on_delete=models.CASCADE, verbose_name=_('content type')
)
object_id = models.PositiveIntegerField(db_index=True, verbose_name=_('object id'))
content_object = GenericForeignKey('content_type', 'object_id')
class Meta:
unique_together = (('journal', 'tag'),)
verbose_name = _('linked object')
def __str__(self):
return u'{0}:{1}:{2}'.format(self.journal.id, self.tag, self.content_object)
return f'{self.journal.id}:{self.tag}:{self.content_object}'

View File

@ -2,12 +2,12 @@
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
from distutils.cmd import Command
from distutils.command.build import build as _build
from setuptools import find_packages, setup
from setuptools.command.install_lib import install_lib as _install_lib
from setuptools.command.sdist import sdist
class test(Command):
@ -22,6 +22,7 @@ class test(Command):
def run(self):
import os
try:
from django.core.management import call_command
except ImportError:
@ -45,6 +46,7 @@ class compile_translations(Command):
try:
os.environ.pop('DJANGO_SETTINGS_MODULE', None)
from django.core.management import call_command
os.chdir(os.path.realpath('django_journal'))
call_command('compilemessages')
except ImportError:
@ -80,16 +82,18 @@ class eo_sdist(sdist):
def get_version():
'''Use the VERSION, if absent generates a version with git describe, if not
tag exists, take 0.0- and add the length of the commit log.
'''
"""Use the VERSION, if absent generates a version with git describe, if not
tag exists, take 0.0- and add the length of the commit log.
"""
if os.path.exists('VERSION'):
with open('VERSION', 'r') as v:
with open('VERSION') as v:
return v.read()
if os.path.exists('.git'):
p = subprocess.Popen(
['git', 'describe', '--dirty=.dirty', '--match=v*'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
result = p.communicate()[0]
if p.returncode == 0:
result = result.decode('ascii').strip()[1:] # strip spaces/newlines and initial v
@ -100,30 +104,27 @@ def get_version():
version = result
return version
else:
return '0.0.post%s' % len(
subprocess.check_output(
['git', 'rev-list', 'HEAD']).splitlines())
return '0.0.post%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines())
return '0.0'
setup(name='django-journal',
version=get_version(),
license='AGPLv3',
description='Keep a structured -- i.e. not just log strings -- journal'
' of events in your applications',
url='http://dev.entrouvert.org/projects/django-journal/',
download_url='http://repos.entrouvert.org/django-journal.git/',
author="Entr'ouvert",
author_email="info@entrouvert.com",
packages=find_packages(os.path.dirname(__file__) or '.'),
include_package_data=True,
cmdclass={
'build': build,
'install_lib': install_lib,
'compile_translations': compile_translations,
'sdist': eo_sdist,
'test': test
},
install_requires=[
'django >= 1.11,<2.3'
])
setup(
name='django-journal',
version=get_version(),
license='AGPLv3',
description='Keep a structured -- i.e. not just log strings -- journal' ' of events in your applications',
url='http://dev.entrouvert.org/projects/django-journal/',
download_url='http://repos.entrouvert.org/django-journal.git/',
author="Entr'ouvert",
author_email="info@entrouvert.com",
packages=find_packages(os.path.dirname(__file__) or '.'),
include_package_data=True,
cmdclass={
'build': build,
'install_lib': install_lib,
'compile_translations': compile_translations,
'sdist': eo_sdist,
'test': test,
},
install_requires=['django >= 1.11,<2.3'],
)

View File

@ -1,16 +1,12 @@
INSTALLED_APPS = (
'django_journal', 'django.contrib.contenttypes', 'django.contrib.auth',
'django.contrib.sessions'
'django_journal',
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.sessions',
)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': '_test'
},
'error': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': '_test'
}
'default': {'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': '_test'},
'error': {'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': '_test'},
}
SECRET_KEY = "django_tests_secret_key"

View File

@ -1,11 +1,10 @@
from django.test import TestCase
from django.contrib.auth.models import User, Group
from django.contrib.auth.models import Group, User
from django.db import transaction
from django.test import TestCase
from django.utils.encoding import force_text
from django_journal.journal import record, error_record
from django_journal.actions import export_as_csv_generator
from django_journal.journal import error_record, record
from django_journal.models import Journal, Tag
@ -15,39 +14,57 @@ class JournalTestCase(TestCase):
self.groups = []
with transaction.atomic():
for i in range(20):
self.users.append(
User.objects.create(username='user%s' % i))
self.users.append(User.objects.create(username='user%s' % i))
for i in range(20):
self.groups.append(
Group.objects.create(name='group%s' % i))
self.groups.append(Group.objects.create(name='group%s' % i))
for i in range(20):
record('login', '{user} logged in', user=self.users[i])
for i in range(20):
record('group-changed', '{user1} gave group {group} to {user2}',
user1=self.users[i], group=self.groups[i],
user2=self.users[(i+1) % 20])
record(
'group-changed',
'{user1} gave group {group} to {user2}',
user1=self.users[i],
group=self.groups[i],
user2=self.users[(i + 1) % 20],
)
for i in range(20):
record('logout', '{user} logged out', user=self.users[i])
def test_login(self):
for i, event in zip(range(20), Journal.objects.for_tag('login').order_by('id')):
self.assertEqual(force_text(event), 'user{0} logged in'.format(i))
self.assertEqual(force_text(event), f'user{i} logged in')
def test_groups(self):
for i, event in zip(range(40), Journal.objects.for_tag('group-changed').order_by('id')):
self.assertEqual(force_text(event),
'user{0} gave group group{0} to user{1}'.format(i, (i+1)%20))
self.assertEqual(
force_text(event), 'user{0} gave group group{0} to user{1}'.format(i, (i + 1) % 20)
)
def test_logout(self):
for i, event in zip(range(20), Journal.objects.for_tag('logout').order_by('id')):
self.assertEqual(force_text(event), 'user{0} logged out'.format(i))
self.assertEqual(force_text(event), f'user{i} logged out')
def test_export_as_csv(self):
qs = Journal.objects.all()
l = list(export_as_csv_generator(qs))
self.assertEquals(set(l[0]), set(['time', 'tag', 'message', 'group', 'group__id', 'user', 'user__id', 'user1', 'user1__id', 'user2', 'user2__id']))
self.assertEquals(
set(l[0]),
{
'time',
'tag',
'message',
'group',
'group__id',
'user',
'user__id',
'user1',
'user1__id',
'user2',
'user2__id',
},
)
l = list(export_as_csv_generator(qs[:5]))
self.assertEquals(set(l[0]), set(['time', 'tag', 'message', 'user', 'user__id']))
self.assertEquals(set(l[0]), {'time', 'tag', 'message', 'user', 'user__id'})
for user in self.users:
user.delete()
qs = Journal.objects.all()
@ -58,8 +75,8 @@ class JournalTestCase(TestCase):
def test_error_record(db):
error_record('error', 'error message')
journal = Journal.objects.first()
assert journal.tag.name == u'error'
assert journal.message == u'error message'
assert journal.tag.name == 'error'
assert journal.message == 'error message'
# specifying None as database use the defaut one
error_record('error', 'error message', using=None)