Projet

Général

Profil

0001-misc-apply-black-54260.patch

Paul Marillonnet, 15 juillet 2021 12:35

Télécharger (38,3 ko)

Voir les différences:

Subject: [PATCH 1/4] misc: apply black (#54260)

 django_journal/actions.py                 |  14 ++-
 django_journal/admin.py                   |  69 +++++++------
 django_journal/decorator.py               |   5 +-
 django_journal/journal.py                 |  49 +++++-----
 django_journal/managers.py                |  54 +++++------
 django_journal/middleware.py              |  13 ++-
 django_journal/migrations/0001_initial.py |  56 +++++++++--
 django_journal/models.py                  | 113 +++++++++++-----------
 setup.py                                  |  57 +++++------
 test_settings.py                          |  16 ++-
 tests/test_main.py                        |  38 ++++++--
 11 files changed, 275 insertions(+), 209 deletions(-)
django_journal/actions.py
8 8

  
9 9
from . import models
10 10

  
11

  
11 12
def export_as_csv_generator(queryset):
12 13
    header = ['time', 'tag', 'message']
13 14
    tags = set(models.Tag.objects.filter(objectdata__journal__in=queryset).values_list('name', flat=True))
......
15 16
        tags.add('%s__id' % tag)
16 17
    tags |= set(models.Tag.objects.filter(stringdata__journal__in=queryset).values_list('name', flat=True))
17 18
    extra_headers = list(sorted(tags))
18
    yield header+extra_headers
19
    yield header + extra_headers
19 20
    for journal in queryset:
20 21
        row = {
21
                'time': journal.time.isoformat(' '),
22
                'tag': force_text(journal.tag.name),
23
                'message': force_text(journal),
24
              }
22
            'time': journal.time.isoformat(' '),
23
            'tag': force_text(journal.tag.name),
24
            'message': force_text(journal),
25
        }
25 26
        for stringdata in journal.stringdata_set.all():
26 27
            row_name = stringdata.tag.name.encode('utf-8')
27 28
            row[force_text(row_name)] = force_text(stringdata.content)
......
34 35
                row[row_name] = force_text(objectdata.content_object)
35 36
        yield row
36 37

  
38

  
37 39
def export_as_csv(modeladmin, request, queryset):
38 40
    """
39 41
    CSV export for journal
......
47 49
    for row in l:
48 50
        writer.writerow(row)
49 51
    return response
52

  
53

  
50 54
export_as_csv.short_description = _(u"Export CSV file")
django_journal/admin.py
13 13

  
14 14

  
15 15
class ModelAdminFormatter(Formatter):
16
    def __init__(self, model_admin=None, filter_link=True,
17
            object_link=True):
16
    def __init__(self, model_admin=None, filter_link=True, object_link=True):
18 17
        self.filter_link = filter_link
19 18
        self.object_link = object_link
20 19
        self.model_admin = model_admin
......
22 21

  
23 22
    def build_object_link(self, value):
24 23
        content_type = ContentType.objects.get_for_model(value.__class__)
25
        url = u'{0}:{1}_{2}_change'.format(self.model_admin.admin_site.name,
26
                content_type.app_label, content_type.model)
24
        url = u'{0}:{1}_{2}_change'.format(
25
            self.model_admin.admin_site.name, content_type.app_label, content_type.model
26
        )
27 27
        try:
28 28
            url = reverse(url, args=(value.pk,))
29 29
        except NoReverseMatch:
......
36 36
            if self.filter_link:
37 37
                content_type = ContentType.objects.get_for_model(value.__class__)
38 38
                res = u'<a href="?objectdata__content_type={0}&objectdata__object_id={1}">{2}</a>'.format(
39
                            content_type.id, value.pk, escape(force_text(value)))
39
                    content_type.id, value.pk, escape(force_text(value))
40
                )
40 41
            else:
41 42
                res = escape(force_text(value))
42 43
            if self.object_link:
......
52 53
    extra = 0
53 54
    max_num = 0
54 55

  
56

  
55 57
class StringDataInlineAdmin(admin.TabularInline):
56 58
    model = StringData
57 59
    fields = ('tag', 'content')
......
59 61
    extra = 0
60 62
    max_num = 0
61 63

  
64

  
62 65
class JournalAdmin(admin.ModelAdmin):
63 66
    list_display = ('time', '_tag', 'user', 'ip', 'message_for_list')
64 67
    list_filter = ('tag',)
65 68
    fields = ('time', 'tag', 'user', 'ip', 'message_for_change')
66 69
    readonly_fields = fields
67 70
    inlines = (
68
            ObjectDataInlineAdmin, 
69
            StringDataInlineAdmin,
71
        ObjectDataInlineAdmin,
72
        StringDataInlineAdmin,
70 73
    )
71 74
    date_hierarchy = 'time'
72
    search_fields = ('message','tag__name','time')
73
    actions = [ export_as_csv ]
75
    search_fields = ('message', 'tag__name', 'time')
76
    actions = [export_as_csv]
74 77

  
75 78
    class Media:
76 79
        css = {
77
                'all': ('journal/css/journal.css',),
80
            'all': ('journal/css/journal.css',),
78 81
        }
79 82

  
80 83
    def queryset(self, request):
81 84
        '''Get as much data as possible using the fewest requests possible.'''
82 85
        qs = super(JournalAdmin, self).queryset(request)
83
        qs = qs.select_related('tag', 'template') \
84
               .prefetch_related('objectdata_set__content_type',
85
                       'stringdata_set', 'objectdata_set__tag',
86
                       'stringdata_set__tag', 'objectdata_set__content_object')
86
        qs = qs.select_related('tag', 'template').prefetch_related(
87
            'objectdata_set__content_type',
88
            'stringdata_set',
89
            'objectdata_set__tag',
90
            'stringdata_set__tag',
91
            'objectdata_set__content_object',
92
        )
87 93
        return qs
88 94

  
89 95
    def lookup_allowed(self, key, *args, **kwargs):
......
91 97

  
92 98
    def _tag(self, entry):
93 99
        name = entry.tag.name.replace(u'-', u'\u2011')
94
        res = format_html('<a href="?tag__id__exact={0}">{1}</a>',
95
                escape(entry.tag.id), escape(name))
100
        res = format_html('<a href="?tag__id__exact={0}">{1}</a>', escape(entry.tag.id), escape(name))
96 101
        return res
102

  
97 103
    _tag.short_description = _('tag')
98 104

  
99 105
    def ip(self, entry):
100 106
        '''Search and return any associated stringdata whose tag is "ip"'''
101 107
        for stringdata in entry.stringdata_set.all():
102 108
            if stringdata.tag.name == 'ip':
103
                return format_html('<a href="?stringdata__tag__id={tag_id}&' \
104
                                   'stringdata__content={ip}">{ip}</a>',
105
                        tag_id=stringdata.tag.id, ip=stringdata.content)
109
                return format_html(
110
                    '<a href="?stringdata__tag__id={tag_id}&' 'stringdata__content={ip}">{ip}</a>',
111
                    tag_id=stringdata.tag.id,
112
                    ip=stringdata.content,
113
                )
106 114
        return _('None')
115

  
107 116
    ip.short_description = _('IP')
108 117

  
109 118
    def user(self, entry):
110 119
        '''Search and return any associated objectdata whose tag is "user"'''
111 120
        for objectdata in entry.objectdata_set.all():
112 121
            if objectdata.tag.name == 'user':
113
                return format_html(self.object_filter_link(objectdata) + \
114
                        self.object_link(objectdata))
122
                return format_html(self.object_filter_link(objectdata) + self.object_link(objectdata))
115 123
        return _('None')
124

  
116 125
    user.short_description = _('User')
117 126

  
118 127
    def object_filter_link(self, objectdata):
......
120 129
            caption = force_text(objectdata.content_object)
121 130
        else:
122 131
            caption = _(u'<deleted {content_type} {object_id}>').format(
123
                    content_type=objectdata.content_type,
124
                    object_id=objectdata.object_id)
132
                content_type=objectdata.content_type, object_id=objectdata.object_id
133
            )
125 134
        return u'<a href="?objectdata__content_type={0}&objectdata__object_id={1}">{2}</a>'.format(
126
                    objectdata.content_type_id,
127
                    objectdata.object_id,
128
                    escape(caption))
135
            objectdata.content_type_id, objectdata.object_id, escape(caption)
136
        )
129 137

  
130 138
    def object_link(self, obj_data):
131 139
        if obj_data.content_object is None:
132 140
            return u''
133
        url = u'{0}:{1}_{2}_change'.format(self.admin_site.name,
134
                obj_data.content_type.app_label,
135
                obj_data.content_type.model)
141
        url = u'{0}:{1}_{2}_change'.format(
142
            self.admin_site.name, obj_data.content_type.app_label, obj_data.content_type.model
143
        )
136 144
        try:
137 145
            url = reverse(url, args=(obj_data.object_id,))
138 146
        except NoReverseMatch:
......
144 152
        formatter = ModelAdminFormatter(model_admin=self, filter_link=False)
145 153
        message = formatter.format(escape(entry.template.content), **ctx)
146 154
        return format_html('<span>{}</span>', mark_safe(message))
155

  
147 156
    message_for_change.short_description = _('Message')
148 157

  
149 158
    def message_for_list(self, entry):
......
151 160
        formatter = ModelAdminFormatter(model_admin=self)
152 161
        message = formatter.format(entry.template.content, **ctx)
153 162
        return format_html('<span>{}</span>', mark_safe(message))
163

  
154 164
    message_for_list.short_description = _('Message')
155 165
    message_for_list.admin_order_field = 'message'
156 166

  
167

  
157 168
admin.site.register(Journal, JournalAdmin)
158 169
admin.site.register(Tag)
django_journal/decorator.py
4 4
if hasattr(transaction, 'atomic'):
5 5
    atomic = transaction.atomic
6 6
else:
7

  
7 8
    class Transaction(object):
8 9
        sid = None
9 10

  
......
41 42
            def wrapper(*args, **kwargs):
42 43
                with self.__class__(using=self.using):
43 44
                    return func(*args, **kwargs)
44
            return wrapper
45 45

  
46
            return wrapper
46 47

  
47 48
    def atomic(using=None):
48 49
        """
......
56 57
        if callable(using):
57 58
            return Transaction(DEFAULT_DB_ALIAS)(using)
58 59
        return Transaction(using)
59

  
60

  
django_journal/journal.py
11 11

  
12 12

  
13 13
def unicode_truncate(s, length, encoding='utf-8'):
14
    '''Truncate an unicode string so that its UTF-8 encoding is less than
15
       length.'''
14
    """Truncate an unicode string so that its UTF-8 encoding is less than
15
    length."""
16 16
    encoded = s.encode(encoding)[:length]
17 17
    return encoded.decode(encoding, 'ignore')
18 18

  
19 19

  
20 20
@atomic
21 21
def record(tag, template, using=None, **kwargs):
22
    '''Record an event in the journal. The modification is done inside the
23
       current transaction.
22
    """Record an event in the journal. The modification is done inside the
23
    current transaction.
24 24

  
25
       tag:
26
           a string identifier giving the type of the event
27
       tpl:
28
           a format string to describe the event
29
       kwargs:
30
           a mapping of object or data to interpolate in the format string
31
    '''
25
    tag:
26
        a string identifier giving the type of the event
27
    tpl:
28
        a format string to describe the event
29
    kwargs:
30
        a mapping of object or data to interpolate in the format string
31
    """
32 32
    template = force_text(template)
33 33
    tag = Tag.objects.using(using).get_cached(name=tag)
34 34
    template = Template.objects.using(using).get_cached(content=template)
35 35
    try:
36 36
        message = template.content.format(**kwargs)
37 37
    except (KeyError, IndexError) as e:
38
        raise JournalException(
39
                'Missing variable for the template message', template, e)
38
        raise JournalException('Missing variable for the template message', template, e)
40 39
    try:
41 40
        logger = logging.getLogger('django.journal.%s' % tag)
42 41
        if tag.name == 'error' or tag.name.startswith('error-'):
......
49 48
        try:
50 49
            logging.getLogger('django.journal').exception('Unable to log msg')
51 50
        except:
52
            pass # we tried, really, we tried
53
    journal = Journal.objects.using(using).create(tag=tag, template=template,
54
            message=unicode_truncate(message, 128))
51
            pass  # we tried, really, we tried
52
    journal = Journal.objects.using(using).create(
53
        tag=tag, template=template, message=unicode_truncate(message, 128)
54
    )
55 55
    for name, value in kwargs.items():
56 56
        if value is None:
57 57
            continue
58 58
        tag = Tag.objects.using(using).get_cached(name=name)
59 59
        if isinstance(value, django.db.models.Model):
60 60
            journal.objectdata_set.create(
61
                tag=tag, content_type=ContentType.objects.db_manager(using).get_for_model(value),
62
                object_id=value.pk
61
                tag=tag,
62
                content_type=ContentType.objects.db_manager(using).get_for_model(value),
63
                object_id=value.pk,
63 64
            )
64 65
        else:
65 66
            journal.stringdata_set.create(tag=tag, content=force_text(value))
......
67 68

  
68 69

  
69 70
def error_record(tag, tpl, **kwargs):
70
    '''Records error events.
71
    """Records error events.
71 72

  
72
       You must use this function when logging error events. It uses another
73
       database alias than the default one to be immune to transaction rollback
74
       when logging in the middle of a transaction which is going to
75
       rollback.
76
    '''
73
    You must use this function when logging error events. It uses another
74
    database alias than the default one to be immune to transaction rollback
75
    when logging in the middle of a transaction which is going to
76
    rollback.
77
    """
77 78
    if kwargs.get('using') is None:
78 79
        kwargs['using'] = getattr(settings, 'JOURNAL_DB_FOR_ERROR_ALIAS', 'default')
79 80

  
django_journal/managers.py
27 27
        '''Return Journal records linked to this object.'''
28 28
        content_type = ContentType.objects.get_for_model(obj)
29 29
        if tag is None:
30
            return self.filter(objectdata__content_type=content_type,
31
                    objectdata__object_id=obj.pk)
30
            return self.filter(objectdata__content_type=content_type, objectdata__object_id=obj.pk)
32 31
        else:
33 32
            return self.filter(
34
                    objectdata__tag__name=tag,
35
                    objectdata__content_type=content_type,
36
                    objectdata__object_id=obj.pk)
33
                objectdata__tag__name=tag, objectdata__content_type=content_type, objectdata__object_id=obj.pk
34
            )
37 35

  
38 36
    def for_objects(self, objects):
39
        '''Return journal records linked to any of this objects.
37
        """Return journal records linked to any of this objects.
40 38

  
41
           All objects must have the same model.
42
        '''
39
        All objects must have the same model.
40
        """
43 41
        if not objects:
44 42
            return self.none()
45
        content_types = [ ContentType.objects.get_for_model(obj)
46
                for obj in objects ]
43
        content_types = [ContentType.objects.get_for_model(obj) for obj in objects]
47 44
        if len(set(content_types)) != 1:
48 45
            raise ValueError('objects must have of the same content type')
49
        pks = [ obj.pk for obj in objects ]
50
        return self.filter(
51
                objectdata__content_type=content_types[0],
52
                objectdata__object_id__in=pks)
46
        pks = [obj.pk for obj in objects]
47
        return self.filter(objectdata__content_type=content_types[0], objectdata__object_id__in=pks)
53 48

  
54 49
    def for_tag(self, tag):
55
        '''Returns Journal records linked to this tag by their own tag or
56
           the tag on their data records.
57
        '''
50
        """Returns Journal records linked to this tag by their own tag or
51
        the tag on their data records.
52
        """
58 53
        from . import models
59 54

  
60 55
        if not isinstance(tag, models.Tag):
......
64 59
                return self.none()
65 60
        # always remember: multiple join (OR in WHERE) produces duplicate
66 61
        # lines ! Use .distinct() for safety.
67
        return self.filter(Q(tag=tag)|
68
                Q(objectdata__tag=tag)|
69
                Q(stringdata__tag=tag)) \
70
                .distinct()
62
        return self.filter(Q(tag=tag) | Q(objectdata__tag=tag) | Q(stringdata__tag=tag)).distinct()
71 63

  
72 64

  
73 65
class JournalManager(Manager.from_queryset(JournalQuerySet)):
74 66
    def get_query_set(self):
75
        return super(JournalManager, self).get_query_set() \
76
               .prefetch_related('objectdata_set__content_type',
77
                       'stringdata_set', 'objectdata_set__tag',
78
                       'stringdata_set__tag', 'objectdata_set__content_object',
79
                       'tag', 'template') \
80
               .select_related('tag', 'template')
67
        return (
68
            super(JournalManager, self)
69
            .get_query_set()
70
            .prefetch_related(
71
                'objectdata_set__content_type',
72
                'stringdata_set',
73
                'objectdata_set__tag',
74
                'stringdata_set__tag',
75
                'objectdata_set__content_object',
76
                'tag',
77
                'template',
78
            )
79
            .select_related('tag', 'template')
80
        )
django_journal/middleware.py
3 3

  
4 4

  
5 5
class JournalMiddleware(MiddlewareMixin):
6
    '''Add record and error_record methods to the request object to log
7
       current user and current REMOTE_ADRESS.
6
    """Add record and error_record methods to the request object to log
7
    current user and current REMOTE_ADRESS.
8 8

  
9
       It must be setup after the auth middleware.
10
    '''
9
    It must be setup after the auth middleware.
10
    """
11 11

  
12 12
    def process_request(self, request):
13 13
        user = getattr(request, 'user', None)
14 14
        ip = request.META.get('REMOTE_ADDR', None)
15

  
15 16
        def record(tag, template, using=None, **kwargs):
16 17
            if 'user' not in kwargs:
17 18
                kwargs['user'] = user
18 19
            if 'ip' not in kwargs:
19 20
                kwargs['ip'] = ip
20
            journal.record(tag, template, using=using,**kwargs)
21
            journal.record(tag, template, using=using, **kwargs)
22

  
21 23
        def error_record(tag, template, using=None, **kwargs):
22 24
            if 'user' not in kwargs:
23 25
                kwargs['user'] = user
24 26
            if 'ip' not in kwargs:
25 27
                kwargs['ip'] = ip
26 28
            journal.error_record(tag, template, using=using, **kwargs)
29

  
27 30
        request.record = record
28 31
        request.error_record = error_record
29 32
        return None
django_journal/migrations/0001_initial.py
15 15
        migrations.CreateModel(
16 16
            name='Journal',
17 17
            fields=[
18
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18
                (
19
                    'id',
20
                    models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
21
                ),
19 22
                ('time', models.DateTimeField(auto_now_add=True, verbose_name='time', db_index=True)),
20 23
                ('message', models.CharField(max_length=128, verbose_name='message', db_index=True)),
21 24
            ],
......
28 31
        migrations.CreateModel(
29 32
            name='ObjectData',
30 33
            fields=[
31
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
34
                (
35
                    'id',
36
                    models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
37
                ),
32 38
                ('object_id', models.PositiveIntegerField(verbose_name='object id', db_index=True)),
33
                ('content_type', models.ForeignKey(verbose_name='content type', to='contenttypes.ContentType', on_delete=models.CASCADE)),
34
                ('journal', models.ForeignKey(verbose_name='journal entry', to='django_journal.Journal', on_delete=models.CASCADE)),
39
                (
40
                    'content_type',
41
                    models.ForeignKey(
42
                        verbose_name='content type', to='contenttypes.ContentType', on_delete=models.CASCADE
43
                    ),
44
                ),
45
                (
46
                    'journal',
47
                    models.ForeignKey(
48
                        verbose_name='journal entry', to='django_journal.Journal', on_delete=models.CASCADE
49
                    ),
50
                ),
35 51
            ],
36 52
            options={
37 53
                'verbose_name': 'linked object',
......
40 56
        migrations.CreateModel(
41 57
            name='StringData',
42 58
            fields=[
43
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
59
                (
60
                    'id',
61
                    models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
62
                ),
44 63
                ('content', models.TextField(verbose_name='content')),
45
                ('journal', models.ForeignKey(verbose_name='journal entry', to='django_journal.Journal', on_delete=models.CASCADE)),
64
                (
65
                    'journal',
66
                    models.ForeignKey(
67
                        verbose_name='journal entry', to='django_journal.Journal', on_delete=models.CASCADE
68
                    ),
69
                ),
46 70
            ],
47 71
            options={
48 72
                'verbose_name': 'linked text string',
......
51 75
        migrations.CreateModel(
52 76
            name='Tag',
53 77
            fields=[
54
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
78
                (
79
                    'id',
80
                    models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
81
                ),
55 82
                ('name', models.CharField(unique=True, max_length=32, verbose_name='name', db_index=True)),
56 83
            ],
57 84
            options={
......
62 89
        migrations.CreateModel(
63 90
            name='Template',
64 91
            fields=[
65
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
92
                (
93
                    'id',
94
                    models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
95
                ),
66 96
                ('content', models.TextField(unique=True, verbose_name='content', db_index=True)),
67 97
            ],
68 98
            options={
......
82 112
        migrations.AddField(
83 113
            model_name='journal',
84 114
            name='tag',
85
            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, verbose_name='tag', to='django_journal.Tag'),
115
            field=models.ForeignKey(
116
                on_delete=django.db.models.deletion.PROTECT, verbose_name='tag', to='django_journal.Tag'
117
            ),
86 118
        ),
87 119
        migrations.AddField(
88 120
            model_name='journal',
89 121
            name='template',
90
            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, verbose_name='template', to='django_journal.Template'),
122
            field=models.ForeignKey(
123
                on_delete=django.db.models.deletion.PROTECT,
124
                verbose_name='template',
125
                to='django_journal.Template',
126
            ),
91 127
        ),
92 128
        migrations.AlterUniqueTogether(
93 129
            name='stringdata',
django_journal/models.py
10 10

  
11 11
@python_2_unicode_compatible
12 12
class Tag(models.Model):
13
    '''Tag allows typing event and data linked to events.
13
    """Tag allows typing event and data linked to events.
14

  
15
    name:
16
        the string identifier of the tag
17
    """
14 18

  
15
       name:
16
           the string identifier of the tag
17
    '''
18 19
    objects = managers.TagManager()
19
    name = models.CharField(verbose_name=_('name'), max_length=32, unique=True,
20
            db_index=True)
20
    name = models.CharField(verbose_name=_('name'), max_length=32, unique=True, db_index=True)
21 21

  
22 22
    def __str__(self):
23 23
        return self.name
......
32 32

  
33 33
@python_2_unicode_compatible
34 34
class Template(models.Model):
35
    '''Template for formatting an event.
35
    """Template for formatting an event.
36

  
37
    ex.: Template(
38
             content='{user1} gave group {group} to {user2}')
39
    """
36 40

  
37
       ex.: Template(
38
                content='{user1} gave group {group} to {user2}')
39
    '''
40 41
    objects = managers.TemplateManager()
41
    content = models.TextField(verbose_name=_('content'), unique=True,
42
            db_index=True)
42
    content = models.TextField(verbose_name=_('content'), unique=True, db_index=True)
43 43

  
44 44
    def __str__(self):
45 45
        return self.content
......
53 53

  
54 54
@python_2_unicode_compatible
55 55
class Journal(models.Model):
56
    '''One line of the journal.
56
    """One line of the journal.
57 57

  
58
       Each recorded event in the journal is a Journal instance.
58
    Each recorded event in the journal is a Journal instance.
59

  
60
    time - the time at which the event was recorded
61
    tag - the tag giving the type of event
62
    template - a format string to present the event
63
    message - a simple string representation of the event, computed using
64
    the template and associated datas.
65
    """
59 66

  
60
       time - the time at which the event was recorded
61
       tag - the tag giving the type of event
62
       template - a format string to present the event
63
       message - a simple string representation of the event, computed using
64
       the template and associated datas.
65
    '''
66 67
    objects = managers.JournalManager()
67 68

  
68
    time = models.DateTimeField(verbose_name=_('time'), auto_now_add=True,
69
            db_index=True)
70
    tag = models.ForeignKey(Tag, verbose_name=_('tag'),
71
            on_delete=models.PROTECT)
72
    template = models.ForeignKey(Template, verbose_name=_('template'),
73
            on_delete=models.PROTECT)
74
    message = models.CharField(verbose_name=_('message'), max_length=128,
75
            db_index=True)
69
    time = models.DateTimeField(verbose_name=_('time'), auto_now_add=True, db_index=True)
70
    tag = models.ForeignKey(Tag, verbose_name=_('tag'), on_delete=models.PROTECT)
71
    template = models.ForeignKey(Template, verbose_name=_('template'), on_delete=models.PROTECT)
72
    message = models.CharField(verbose_name=_('message'), max_length=128, db_index=True)
76 73

  
77 74
    class Meta:
78 75
        ordering = ('-id',)
......
86 83
                ctx[data.tag.name] = data.content_object
87 84
            else:
88 85
                ctx[data.tag.name] = u'<deleted {content_type} {object_id}>'.format(
89
                        content_type=data.content_type, object_id=data.object_id)
86
                    content_type=data.content_type, object_id=data.object_id
87
                )
90 88
        for data in self.stringdata_set.all():
91 89
            ctx[data.tag.name] = data.content
92 90
        for text, field, format_spec, conversion in string.Formatter().parse(self.template.content):
......
98 96
        return ctx
99 97

  
100 98
    def add_object_tag(self, tag_name, obj):
101
        ObjectData(journal=self,
102
                tag=Tag.objects.get_cached(name=tag_name),
103
                content_object=obj).save()
99
        ObjectData(journal=self, tag=Tag.objects.get_cached(name=tag_name), content_object=obj).save()
104 100

  
105 101
    def __str__(self):
106 102
        ctx = self.message_context()
......
108 104

  
109 105
    def __repr__(self):
110 106
        return '<Journal pk:{0} tag:{1} message:{2}>'.format(
111
                self.pk, unicode(self.tag).encode('utf-8'),
112
                unicode(self.message).encode('utf-8'))
107
            self.pk, unicode(self.tag).encode('utf-8'), unicode(self.message).encode('utf-8')
108
        )
113 109

  
114 110

  
115 111
class StringData(models.Model):
116
    '''String data associated to a recorded event.
117

  
118
       journal:
119
           the recorded event
120
       tag:
121
           the identifier for this data
122
       content:
123
           the string value of the data
124
    '''
112
    """String data associated to a recorded event.
113

  
114
    journal:
115
        the recorded event
116
    tag:
117
        the identifier for this data
118
    content:
119
        the string value of the data
120
    """
121

  
125 122
    journal = models.ForeignKey(Journal, verbose_name=_('journal entry'), on_delete=models.CASCADE)
126 123
    tag = models.ForeignKey(Tag, verbose_name=_('tag'), on_delete=models.CASCADE)
127 124
    content = models.TextField(verbose_name=_('content'))
......
133 130

  
134 131
@python_2_unicode_compatible
135 132
class ObjectData(models.Model):
136
    '''Object data associated with a recorded event.
137

  
138
       journal:
139
           the recorded event
140
       tag:
141
           the identifier for this data
142
       content_object:
143
           the object value of the data
144
    '''
133
    """Object data associated with a recorded event.
134

  
135
    journal:
136
        the recorded event
137
    tag:
138
        the identifier for this data
139
    content_object:
140
        the object value of the data
141
    """
142

  
145 143
    journal = models.ForeignKey(Journal, verbose_name=_('journal entry'), on_delete=models.CASCADE)
146 144
    tag = models.ForeignKey(Tag, verbose_name=_('tag'), on_delete=models.CASCADE)
147
    content_type = models.ForeignKey('contenttypes.ContentType', on_delete=models.CASCADE,
148
            verbose_name=_('content type'))
149
    object_id = models.PositiveIntegerField(db_index=True,
150
            verbose_name=_('object id'))
151
    content_object = GenericForeignKey('content_type',
152
            'object_id')
145
    content_type = models.ForeignKey(
146
        'contenttypes.ContentType', on_delete=models.CASCADE, verbose_name=_('content type')
147
    )
148
    object_id = models.PositiveIntegerField(db_index=True, verbose_name=_('object id'))
149
    content_object = GenericForeignKey('content_type', 'object_id')
153 150

  
154 151
    class Meta:
155 152
        unique_together = (('journal', 'tag'),)
setup.py
22 22

  
23 23
    def run(self):
24 24
        import os
25

  
25 26
        try:
26 27
            from django.core.management import call_command
27 28
        except ImportError:
......
45 46
        try:
46 47
            os.environ.pop('DJANGO_SETTINGS_MODULE', None)
47 48
            from django.core.management import call_command
49

  
48 50
            os.chdir(os.path.realpath('django_journal'))
49 51
            call_command('compilemessages')
50 52
        except ImportError:
......
80 82

  
81 83

  
82 84
def get_version():
83
    '''Use the VERSION, if absent generates a version with git describe, if not
84
       tag exists, take 0.0- and add the length of the commit log.
85
    '''
85
    """Use the VERSION, if absent generates a version with git describe, if not
86
    tag exists, take 0.0- and add the length of the commit log.
87
    """
86 88
    if os.path.exists('VERSION'):
87 89
        with open('VERSION', 'r') as v:
88 90
            return v.read()
89 91
    if os.path.exists('.git'):
90 92
        p = subprocess.Popen(
91 93
            ['git', 'describe', '--dirty=.dirty', '--match=v*'],
92
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
94
            stdout=subprocess.PIPE,
95
            stderr=subprocess.PIPE,
96
        )
93 97
        result = p.communicate()[0]
94 98
        if p.returncode == 0:
95 99
            result = result.decode('ascii').strip()[1:]  # strip spaces/newlines and initial v
......
100 104
                version = result
101 105
            return version
102 106
        else:
103
            return '0.0.post%s' % len(
104
                    subprocess.check_output(
105
                            ['git', 'rev-list', 'HEAD']).splitlines())
107
            return '0.0.post%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines())
106 108
    return '0.0'
107 109

  
108 110

  
109
setup(name='django-journal',
110
      version=get_version(),
111
      license='AGPLv3',
112
      description='Keep a structured -- i.e. not just log strings -- journal'
113
                  ' of events in your applications',
114
      url='http://dev.entrouvert.org/projects/django-journal/',
115
      download_url='http://repos.entrouvert.org/django-journal.git/',
116
      author="Entr'ouvert",
117
      author_email="info@entrouvert.com",
118
      packages=find_packages(os.path.dirname(__file__) or '.'),
119
      include_package_data=True,
120
      cmdclass={
121
          'build': build,
122
          'install_lib': install_lib,
123
          'compile_translations': compile_translations,
124
          'sdist': eo_sdist,
125
          'test': test
126
      },
127
      install_requires=[
128
          'django >= 1.11,<2.3'
129
      ])
111
setup(
112
    name='django-journal',
113
    version=get_version(),
114
    license='AGPLv3',
115
    description='Keep a structured -- i.e. not just log strings -- journal' ' of events in your applications',
116
    url='http://dev.entrouvert.org/projects/django-journal/',
117
    download_url='http://repos.entrouvert.org/django-journal.git/',
118
    author="Entr'ouvert",
119
    author_email="info@entrouvert.com",
120
    packages=find_packages(os.path.dirname(__file__) or '.'),
121
    include_package_data=True,
122
    cmdclass={
123
        'build': build,
124
        'install_lib': install_lib,
125
        'compile_translations': compile_translations,
126
        'sdist': eo_sdist,
127
        'test': test,
128
    },
129
    install_requires=['django >= 1.11,<2.3'],
130
)
test_settings.py
1 1
INSTALLED_APPS = (
2
    'django_journal', 'django.contrib.contenttypes', 'django.contrib.auth',
3
    'django.contrib.sessions'
2
    'django_journal',
3
    'django.contrib.contenttypes',
4
    'django.contrib.auth',
5
    'django.contrib.sessions',
4 6
)
5 7
DATABASES = {
6
    'default': {
7
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
8
        'NAME': '_test'
9
    },
10
    'error': {
11
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
12
        'NAME': '_test'
13
    }
8
    'default': {'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': '_test'},
9
    'error': {'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': '_test'},
14 10
}
15 11

  
16 12
SECRET_KEY = "django_tests_secret_key"
tests/test_main.py
15 15
        self.groups = []
16 16
        with transaction.atomic():
17 17
            for i in range(20):
18
                self.users.append(
19
                        User.objects.create(username='user%s' % i))
18
                self.users.append(User.objects.create(username='user%s' % i))
20 19
            for i in range(20):
21
                self.groups.append(
22
                        Group.objects.create(name='group%s' % i))
20
                self.groups.append(Group.objects.create(name='group%s' % i))
23 21
            for i in range(20):
24 22
                record('login', '{user} logged in', user=self.users[i])
25 23
            for i in range(20):
26
                record('group-changed', '{user1} gave group {group} to {user2}',
27
                        user1=self.users[i], group=self.groups[i], 
28
                        user2=self.users[(i+1) % 20])
24
                record(
25
                    'group-changed',
26
                    '{user1} gave group {group} to {user2}',
27
                    user1=self.users[i],
28
                    group=self.groups[i],
29
                    user2=self.users[(i + 1) % 20],
30
                )
29 31
            for i in range(20):
30 32
                record('logout', '{user} logged out', user=self.users[i])
31 33

  
......
35 37

  
36 38
    def test_groups(self):
37 39
        for i, event in zip(range(40), Journal.objects.for_tag('group-changed').order_by('id')):
38
            self.assertEqual(force_text(event),
39
                    'user{0} gave group group{0} to user{1}'.format(i, (i+1)%20))
40
            self.assertEqual(
41
                force_text(event), 'user{0} gave group group{0} to user{1}'.format(i, (i + 1) % 20)
42
            )
40 43

  
41 44
    def test_logout(self):
42 45
        for i, event in zip(range(20), Journal.objects.for_tag('logout').order_by('id')):
......
45 48
    def test_export_as_csv(self):
46 49
        qs = Journal.objects.all()
47 50
        l = list(export_as_csv_generator(qs))
48
        self.assertEquals(set(l[0]), set(['time', 'tag', 'message', 'group', 'group__id', 'user', 'user__id', 'user1', 'user1__id', 'user2', 'user2__id']))
51
        self.assertEquals(
52
            set(l[0]),
53
            {
54
                'time',
55
                'tag',
56
                'message',
57
                'group',
58
                'group__id',
59
                'user',
60
                'user__id',
61
                'user1',
62
                'user1__id',
63
                'user2',
64
                'user2__id',
65
            },
66
        )
49 67
        l = list(export_as_csv_generator(qs[:5]))
50 68
        self.assertEquals(set(l[0]), set(['time', 'tag', 'message', 'user', 'user__id']))
51 69
        for user in self.users:
52
-