misc: apply black/isort/pyupgrade (#54260)
This commit is contained in:
parent
0855ad22c3
commit
7233202758
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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')
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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}'
|
||||
|
|
69
setup.py
69
setup.py
|
@ -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'],
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue