accrete 0.0.111__py3-none-any.whl → 0.0.113__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- accrete/contrib/log/__init__.py +0 -0
- accrete/contrib/log/admin.py +43 -0
- accrete/contrib/log/apps.py +13 -0
- accrete/contrib/log/helper.py +79 -0
- accrete/contrib/log/migrations/0001_initial.py +38 -0
- accrete/contrib/log/migrations/0002_log_user_alter_log_new_value_type_and_more.py +31 -0
- accrete/contrib/log/migrations/0003_alter_log_tenant.py +20 -0
- accrete/contrib/log/migrations/0004_logconfig_logconfigfield.py +47 -0
- accrete/contrib/log/migrations/0005_logconfig_exclude_fields_and_more.py +29 -0
- accrete/contrib/log/migrations/__init__.py +0 -0
- accrete/contrib/log/models.py +229 -0
- accrete/contrib/log/queries.py +34 -0
- accrete/contrib/log/signals.py +62 -0
- accrete/contrib/log/tests.py +3 -0
- accrete/contrib/log/views.py +3 -0
- accrete/contrib/sequence/migrations/0003_alter_sequence_tenant.py +20 -0
- accrete/contrib/ui/templates/ui/form_error.html +11 -0
- accrete/managers.py +7 -0
- accrete/migrations/0003_remove_member_name.py +17 -0
- accrete/models.py +4 -20
- {accrete-0.0.111.dist-info → accrete-0.0.113.dist-info}/METADATA +1 -1
- {accrete-0.0.111.dist-info → accrete-0.0.113.dist-info}/RECORD +24 -7
- accrete/annotation.py +0 -46
- {accrete-0.0.111.dist-info → accrete-0.0.113.dist-info}/WHEEL +0 -0
- {accrete-0.0.111.dist-info → accrete-0.0.113.dist-info}/licenses/LICENSE +0 -0
File without changes
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import django.apps
|
2
|
+
from django.contrib import admin
|
3
|
+
from django import forms
|
4
|
+
from . import models
|
5
|
+
|
6
|
+
installed_models = django.apps.apps.get_models(
|
7
|
+
include_auto_created=True, include_swapped=True
|
8
|
+
)
|
9
|
+
model_choices = [(
|
10
|
+
f'{m._meta.app_label}.{m._meta.model_name}',
|
11
|
+
f'{m._meta.app_label}.{m._meta.verbose_name}'
|
12
|
+
) for m in installed_models]
|
13
|
+
|
14
|
+
|
15
|
+
class LogConfigForm(forms.ModelForm):
|
16
|
+
|
17
|
+
model = forms.ChoiceField(choices=model_choices)
|
18
|
+
|
19
|
+
|
20
|
+
class LogConfigFieldInLine(admin.TabularInline):
|
21
|
+
|
22
|
+
model = models.LogConfigField
|
23
|
+
|
24
|
+
|
25
|
+
class LogConfigAdmin(admin.ModelAdmin):
|
26
|
+
|
27
|
+
model = models.LogConfig
|
28
|
+
list_display = ('model', 'ignore_errors')
|
29
|
+
search_fields = ('pk', 'model')
|
30
|
+
list_filter = ['ignore_errors']
|
31
|
+
form = LogConfigForm
|
32
|
+
inlines = [LogConfigFieldInLine]
|
33
|
+
|
34
|
+
def formfield_for_choice_field(self, db_field, request, **kwargs):
|
35
|
+
if db_field.name == "model":
|
36
|
+
kwargs["choices"] = [
|
37
|
+
("accepted", "Accepted"),
|
38
|
+
("denied", "Denied"),
|
39
|
+
]
|
40
|
+
return super().formfield_for_choice_field(db_field, request, **kwargs)
|
41
|
+
|
42
|
+
|
43
|
+
admin.site.register(models.LogConfig, LogConfigAdmin)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from django.apps import AppConfig
|
2
|
+
from django.db.models.signals import post_save
|
3
|
+
|
4
|
+
|
5
|
+
class LogConfig(AppConfig):
|
6
|
+
default_auto_field = 'django.db.models.BigAutoField'
|
7
|
+
name = 'accrete.contrib.log'
|
8
|
+
|
9
|
+
def ready(self):
|
10
|
+
from . import signals
|
11
|
+
post_save.connect(
|
12
|
+
signals.create_log, weak=False, dispatch_uid="accrete.contrib.log"
|
13
|
+
)
|
@@ -0,0 +1,79 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from django.db.models.query import RawQuerySet
|
3
|
+
from django.forms import model_to_dict
|
4
|
+
|
5
|
+
from .models import Log, LogConfig
|
6
|
+
|
7
|
+
TYPES_FK = ['AutoField', 'BigAutoField', 'ForeignKey']
|
8
|
+
TYPES_INT = ['IntegerField', 'PositiveSmallIntegerField']
|
9
|
+
TYPES_DECIMAL = ['DecimalField']
|
10
|
+
TYPES_FLOAT = ['FloatField']
|
11
|
+
TYPES_STR = ['CharField', 'TextField']
|
12
|
+
TYPES_BOOL = ['BooleanField']
|
13
|
+
TYPES_DATETIME = ['DateTimeField']
|
14
|
+
TYPES_DATE = ['DateField']
|
15
|
+
TYPES_TIME = ['TimeField']
|
16
|
+
|
17
|
+
|
18
|
+
def internal_type_to_log_type(field: models.Field):
|
19
|
+
internal_type = field.get_internal_type()
|
20
|
+
if internal_type in TYPES_FK:
|
21
|
+
return 'fk'
|
22
|
+
if internal_type in TYPES_INT:
|
23
|
+
return 'int'
|
24
|
+
if internal_type in TYPES_FLOAT:
|
25
|
+
return 'float'
|
26
|
+
if internal_type in TYPES_DECIMAL:
|
27
|
+
return 'decimal'
|
28
|
+
if internal_type in TYPES_BOOL:
|
29
|
+
return 'bool'
|
30
|
+
if internal_type in TYPES_STR:
|
31
|
+
return 'str'
|
32
|
+
if internal_type in TYPES_DATE:
|
33
|
+
return 'date'
|
34
|
+
if internal_type in TYPES_DATETIME:
|
35
|
+
return 'datetime'
|
36
|
+
|
37
|
+
|
38
|
+
def log_state_to_dict(logs: RawQuerySet) -> tuple[dict, dict]:
|
39
|
+
state = dict()
|
40
|
+
info = dict()
|
41
|
+
if not logs:
|
42
|
+
return state, info
|
43
|
+
for log in logs:
|
44
|
+
state.update({log.field: log.cast_value()})
|
45
|
+
info.update({log.field: {'new_value_type': log.new_value_type}})
|
46
|
+
return state, info
|
47
|
+
|
48
|
+
|
49
|
+
def log_value_to_instance_value(log: Log):
|
50
|
+
if log.new_value_type == 'bool':
|
51
|
+
return bool(log.new_value == 'True')
|
52
|
+
|
53
|
+
if log.new_value == '':
|
54
|
+
return None
|
55
|
+
if log.new_value_type in ['fk', 'int']:
|
56
|
+
return int(log.new_value)
|
57
|
+
|
58
|
+
|
59
|
+
def get_instance_state(instance: models.Model):
|
60
|
+
state = model_to_dict(instance)
|
61
|
+
cleaned_state = dict()
|
62
|
+
log_config = LogConfig.objects.filter(
|
63
|
+
model=f'{instance._meta.app_label}.{instance._meta.model_name}'
|
64
|
+
).prefetch_related('fields').first()
|
65
|
+
if not log_config:
|
66
|
+
return cleaned_state
|
67
|
+
all_fields = list(map(lambda x: x.name, instance._meta.get_fields()))
|
68
|
+
if log_config.exclude_fields:
|
69
|
+
fields_to_log = list(f for f in all_fields if f not in log_config.fields.all().values_list('field_name', flat=True))
|
70
|
+
else:
|
71
|
+
fields_to_log = log_config.fields.all().values_list('field_name', flat=True)
|
72
|
+
|
73
|
+
for f, v in state.items():
|
74
|
+
if isinstance(v, (list, tuple, dict)):
|
75
|
+
continue
|
76
|
+
if f not in fields_to_log:
|
77
|
+
continue
|
78
|
+
cleaned_state.update({f: v})
|
79
|
+
return cleaned_state
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# Generated by Django 5.1.1 on 2025-01-08 20:04
|
2
|
+
|
3
|
+
import django.db.models.deletion
|
4
|
+
from django.db import migrations, models
|
5
|
+
|
6
|
+
|
7
|
+
class Migration(migrations.Migration):
|
8
|
+
|
9
|
+
initial = True
|
10
|
+
|
11
|
+
dependencies = [
|
12
|
+
('accrete', '0002_initial'),
|
13
|
+
]
|
14
|
+
|
15
|
+
operations = [
|
16
|
+
migrations.CreateModel(
|
17
|
+
name='Log',
|
18
|
+
fields=[
|
19
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
20
|
+
('model', models.CharField(help_text='Combination of app label and model name seperated by a dot', max_length=255, verbose_name='Model')),
|
21
|
+
('field', models.CharField(max_length=255, verbose_name='Field')),
|
22
|
+
('object_id', models.BigIntegerField(verbose_name='Object ID')),
|
23
|
+
('log_date', models.DateTimeField(auto_now_add=True, verbose_name='Date')),
|
24
|
+
('old_value_type', models.CharField(choices=[('fk', 'Foreign Key'), ('int', 'Integer'), ('float', 'Float'), ('decimal', 'Decimal'), ('bool', 'Boolean'), ('str', 'String')], max_length=100, verbose_name='Old Value Type')),
|
25
|
+
('new_value_type', models.CharField(choices=[('fk', 'Foreign Key'), ('int', 'Integer'), ('float', 'Float'), ('decimal', 'Decimal'), ('bool', 'Boolean'), ('str', 'String')], max_length=100, verbose_name='New Value Type')),
|
26
|
+
('old_value', models.TextField(blank=True, null=True, verbose_name='Old Value')),
|
27
|
+
('new_value', models.TextField(blank=True, null=True, verbose_name='New Value')),
|
28
|
+
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accrete.tenant', verbose_name='Tenant')),
|
29
|
+
],
|
30
|
+
options={
|
31
|
+
'verbose_name': 'Log',
|
32
|
+
'verbose_name_plural': 'Logs',
|
33
|
+
'db_table': 'accrete_log',
|
34
|
+
'indexes': [models.Index(fields=['model', 'object_id'], name='accrete_log_model_cf888c_idx')],
|
35
|
+
'constraints': [models.UniqueConstraint(fields=('model', 'field', 'object_id', 'log_date'), name='unique_group_fields')],
|
36
|
+
},
|
37
|
+
),
|
38
|
+
]
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Generated by Django 5.1.1 on 2025-01-10 07:56
|
2
|
+
|
3
|
+
import django.db.models.deletion
|
4
|
+
from django.conf import settings
|
5
|
+
from django.db import migrations, models
|
6
|
+
|
7
|
+
|
8
|
+
class Migration(migrations.Migration):
|
9
|
+
|
10
|
+
dependencies = [
|
11
|
+
('log', '0001_initial'),
|
12
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
13
|
+
]
|
14
|
+
|
15
|
+
operations = [
|
16
|
+
migrations.AddField(
|
17
|
+
model_name='log',
|
18
|
+
name='user',
|
19
|
+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='logs', to=settings.AUTH_USER_MODEL),
|
20
|
+
),
|
21
|
+
migrations.AlterField(
|
22
|
+
model_name='log',
|
23
|
+
name='new_value_type',
|
24
|
+
field=models.CharField(choices=[('fk', 'Foreign Key'), ('int', 'Integer'), ('float', 'Float'), ('decimal', 'Decimal'), ('bool', 'Boolean'), ('str', 'String'), ('date', 'Date'), ('datetime', 'Date Time')], max_length=100, verbose_name='New Value Type'),
|
25
|
+
),
|
26
|
+
migrations.AlterField(
|
27
|
+
model_name='log',
|
28
|
+
name='old_value_type',
|
29
|
+
field=models.CharField(choices=[('fk', 'Foreign Key'), ('int', 'Integer'), ('float', 'Float'), ('decimal', 'Decimal'), ('bool', 'Boolean'), ('str', 'String'), ('date', 'Date'), ('datetime', 'Date Time')], max_length=100, verbose_name='Old Value Type'),
|
30
|
+
),
|
31
|
+
]
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Generated by Django 5.1.1 on 2025-01-10 08:02
|
2
|
+
|
3
|
+
import django.db.models.deletion
|
4
|
+
from django.db import migrations, models
|
5
|
+
|
6
|
+
|
7
|
+
class Migration(migrations.Migration):
|
8
|
+
|
9
|
+
dependencies = [
|
10
|
+
('accrete', '0003_remove_member_name'),
|
11
|
+
('log', '0002_log_user_alter_log_new_value_type_and_more'),
|
12
|
+
]
|
13
|
+
|
14
|
+
operations = [
|
15
|
+
migrations.AlterField(
|
16
|
+
model_name='log',
|
17
|
+
name='tenant',
|
18
|
+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='accrete.tenant', verbose_name='Tenant'),
|
19
|
+
),
|
20
|
+
]
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# Generated by Django 5.1.1 on 2025-01-10 15:20
|
2
|
+
|
3
|
+
import django.db.models.deletion
|
4
|
+
import django.db.models.functions.text
|
5
|
+
from django.db import migrations, models
|
6
|
+
|
7
|
+
|
8
|
+
class Migration(migrations.Migration):
|
9
|
+
|
10
|
+
dependencies = [
|
11
|
+
('log', '0003_alter_log_tenant'),
|
12
|
+
]
|
13
|
+
|
14
|
+
operations = [
|
15
|
+
migrations.CreateModel(
|
16
|
+
name='LogConfig',
|
17
|
+
fields=[
|
18
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
19
|
+
('model', models.CharField(help_text='Combination of app label and model name seperated by a dot', max_length=255, verbose_name='Model')),
|
20
|
+
('ignore_errors', models.BooleanField(default=False, help_text='IF true, exceptions during log creation will be ignored', verbose_name='Ignore Errors')),
|
21
|
+
],
|
22
|
+
options={
|
23
|
+
'verbose_name': 'Log Configuration',
|
24
|
+
'verbose_name_plural': 'Log Configs',
|
25
|
+
'db_table': 'accrete_log_config',
|
26
|
+
'ordering': [django.db.models.functions.text.Lower('model')],
|
27
|
+
'indexes': [models.Index(fields=['model'], name='accrete_log_model_a89071_idx')],
|
28
|
+
'constraints': [models.UniqueConstraint(fields=('model',), name='unique_model')],
|
29
|
+
},
|
30
|
+
),
|
31
|
+
migrations.CreateModel(
|
32
|
+
name='LogConfigField',
|
33
|
+
fields=[
|
34
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
35
|
+
('field_name', models.CharField(help_text='Name of the field to log', max_length=255, verbose_name='Field Name')),
|
36
|
+
('log_config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='log.logconfig', verbose_name='Log Configuration')),
|
37
|
+
],
|
38
|
+
options={
|
39
|
+
'verbose_name': 'Log Configuration Field',
|
40
|
+
'verbose_name_plural': 'Log Configuration Fields',
|
41
|
+
'db_table': 'accrete_log_config_field',
|
42
|
+
'ordering': [django.db.models.functions.text.Lower('field_name')],
|
43
|
+
'indexes': [models.Index(fields=['log_config'], name='accrete_log_log_con_22f45e_idx')],
|
44
|
+
'constraints': [models.UniqueConstraint(fields=('log_config', 'field_name'), name='unique_config_field_name')],
|
45
|
+
},
|
46
|
+
),
|
47
|
+
]
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Generated by Django 5.1.1 on 2025-01-12 09:24
|
2
|
+
|
3
|
+
import django.db.models.deletion
|
4
|
+
from django.db import migrations, models
|
5
|
+
|
6
|
+
|
7
|
+
class Migration(migrations.Migration):
|
8
|
+
|
9
|
+
dependencies = [
|
10
|
+
('log', '0004_logconfig_logconfigfield'),
|
11
|
+
]
|
12
|
+
|
13
|
+
operations = [
|
14
|
+
migrations.AddField(
|
15
|
+
model_name='logconfig',
|
16
|
+
name='exclude_fields',
|
17
|
+
field=models.BooleanField(default=False, help_text='If set, Log Configuration Fields will be excluded from logging. Otherwise only configured fields will be logged.', verbose_name='Exclude Fields'),
|
18
|
+
),
|
19
|
+
migrations.AlterField(
|
20
|
+
model_name='logconfig',
|
21
|
+
name='ignore_errors',
|
22
|
+
field=models.BooleanField(default=False, help_text='If set, exceptions during log creation will be ignored', verbose_name='Ignore Errors'),
|
23
|
+
),
|
24
|
+
migrations.AlterField(
|
25
|
+
model_name='logconfigfield',
|
26
|
+
name='log_config',
|
27
|
+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='log.logconfig', verbose_name='Log Configuration'),
|
28
|
+
),
|
29
|
+
]
|
File without changes
|
@@ -0,0 +1,229 @@
|
|
1
|
+
from decimal import Decimal
|
2
|
+
from datetime import date, datetime
|
3
|
+
|
4
|
+
from django.conf import settings
|
5
|
+
from django.db.models.functions import Lower
|
6
|
+
from django.utils.translation import gettext_lazy as _
|
7
|
+
from django.db import models
|
8
|
+
|
9
|
+
|
10
|
+
class Log(models.Model):
|
11
|
+
|
12
|
+
class Meta:
|
13
|
+
verbose_name = _('Log')
|
14
|
+
verbose_name_plural = _('Logs')
|
15
|
+
db_table = 'accrete_log'
|
16
|
+
indexes = [
|
17
|
+
models.Index(fields=['model', 'object_id'])
|
18
|
+
]
|
19
|
+
constraints = [
|
20
|
+
models.UniqueConstraint(
|
21
|
+
name='unique_group_fields',
|
22
|
+
fields=('model', 'field', 'object_id', 'log_date')
|
23
|
+
)
|
24
|
+
]
|
25
|
+
|
26
|
+
tenant = models.ForeignKey(
|
27
|
+
verbose_name=_('Tenant'),
|
28
|
+
to='accrete.Tenant',
|
29
|
+
on_delete=models.CASCADE,
|
30
|
+
null=True,
|
31
|
+
blank=True
|
32
|
+
)
|
33
|
+
|
34
|
+
model = models.CharField(
|
35
|
+
verbose_name=_('Model'),
|
36
|
+
max_length=255,
|
37
|
+
help_text='Combination of app label and model name seperated by a dot'
|
38
|
+
)
|
39
|
+
|
40
|
+
field = models.CharField(
|
41
|
+
verbose_name=_('Field'),
|
42
|
+
max_length=255
|
43
|
+
)
|
44
|
+
|
45
|
+
object_id = models.BigIntegerField(
|
46
|
+
verbose_name='Object ID'
|
47
|
+
)
|
48
|
+
|
49
|
+
log_date = models.DateTimeField(
|
50
|
+
verbose_name=_('Date'),
|
51
|
+
auto_now_add=True
|
52
|
+
)
|
53
|
+
|
54
|
+
old_value_type = models.CharField(
|
55
|
+
verbose_name=_('Old Value Type'),
|
56
|
+
choices=[
|
57
|
+
('fk', 'Foreign Key'),
|
58
|
+
('int', 'Integer'),
|
59
|
+
('float', 'Float'),
|
60
|
+
('decimal', 'Decimal'),
|
61
|
+
('bool', 'Boolean'),
|
62
|
+
('str', 'String'),
|
63
|
+
('date', 'Date'),
|
64
|
+
('datetime', 'Date Time')
|
65
|
+
],
|
66
|
+
max_length=100,
|
67
|
+
)
|
68
|
+
|
69
|
+
new_value_type = models.CharField(
|
70
|
+
verbose_name=_('New Value Type'),
|
71
|
+
choices=[
|
72
|
+
('fk', 'Foreign Key'),
|
73
|
+
('int', 'Integer'),
|
74
|
+
('float', 'Float'),
|
75
|
+
('decimal', 'Decimal'),
|
76
|
+
('bool', 'Boolean'),
|
77
|
+
('str', 'String'),
|
78
|
+
('date', 'Date'),
|
79
|
+
('datetime', 'Date Time')
|
80
|
+
],
|
81
|
+
max_length=100
|
82
|
+
)
|
83
|
+
|
84
|
+
old_value = models.TextField(
|
85
|
+
verbose_name=_('Old Value'),
|
86
|
+
null=True,
|
87
|
+
blank=True
|
88
|
+
)
|
89
|
+
|
90
|
+
new_value = models.TextField(
|
91
|
+
verbose_name=_('New Value'),
|
92
|
+
null=True,
|
93
|
+
blank=True
|
94
|
+
)
|
95
|
+
|
96
|
+
user = models.ForeignKey(
|
97
|
+
to=settings.AUTH_USER_MODEL,
|
98
|
+
related_name='logs',
|
99
|
+
on_delete=models.SET_NULL,
|
100
|
+
null=True,
|
101
|
+
blank=True
|
102
|
+
)
|
103
|
+
|
104
|
+
def __str__(self):
|
105
|
+
return f'{self.model}.{self.field},{self.object_id}'
|
106
|
+
|
107
|
+
def cast_value(self):
|
108
|
+
if self.new_value is None:
|
109
|
+
return None
|
110
|
+
if self.new_value_type == 'fk':
|
111
|
+
return self.cast_fk()
|
112
|
+
if self.new_value_type == 'int':
|
113
|
+
return self.cast_int()
|
114
|
+
if self.new_value_type == 'float':
|
115
|
+
return self.cast_float()
|
116
|
+
if self.new_value_type == 'decimal':
|
117
|
+
return self.cast_decimal()
|
118
|
+
if self.new_value_type == 'bool':
|
119
|
+
return self.cast_bool()
|
120
|
+
if self.new_value_type == 'str':
|
121
|
+
return self.cast_str()
|
122
|
+
if self.new_value_type == 'date':
|
123
|
+
return self.cast_date()
|
124
|
+
if self.new_value_type == 'datetime':
|
125
|
+
return self.cast_date_time()
|
126
|
+
|
127
|
+
def cast_fk(self):
|
128
|
+
return self.cast_int()
|
129
|
+
|
130
|
+
def cast_int(self):
|
131
|
+
if self.new_value == '':
|
132
|
+
return None
|
133
|
+
return int(self.new_value)
|
134
|
+
|
135
|
+
def cast_float(self):
|
136
|
+
if self.new_value == '':
|
137
|
+
return 0.0
|
138
|
+
return float(self.new_value)
|
139
|
+
|
140
|
+
def cast_decimal(self):
|
141
|
+
if self.new_value == '':
|
142
|
+
return Decimal(0)
|
143
|
+
return Decimal(self.new_value)
|
144
|
+
|
145
|
+
def cast_bool(self):
|
146
|
+
return self.new_value == 'True'
|
147
|
+
|
148
|
+
def cast_str(self):
|
149
|
+
return self.new_value
|
150
|
+
|
151
|
+
def cast_date(self):
|
152
|
+
if self.new_value == '':
|
153
|
+
return None
|
154
|
+
return date.fromisoformat(str(self.new_value))
|
155
|
+
|
156
|
+
def cast_date_time(self):
|
157
|
+
if self.new_value == '':
|
158
|
+
return None
|
159
|
+
return datetime.fromisoformat(self.new_value)
|
160
|
+
|
161
|
+
|
162
|
+
class LogConfig(models.Model):
|
163
|
+
|
164
|
+
class Meta:
|
165
|
+
verbose_name = _('Log Configuration')
|
166
|
+
verbose_name_plural = _('Log Configs')
|
167
|
+
db_table = 'accrete_log_config'
|
168
|
+
ordering = [Lower('model')]
|
169
|
+
indexes = [
|
170
|
+
models.Index(fields=['model'])
|
171
|
+
]
|
172
|
+
constraints = [
|
173
|
+
models.UniqueConstraint(
|
174
|
+
name='unique_model',
|
175
|
+
fields=('model',)
|
176
|
+
)
|
177
|
+
]
|
178
|
+
|
179
|
+
model = models.CharField(
|
180
|
+
verbose_name=_('Model'),
|
181
|
+
max_length=255,
|
182
|
+
help_text=_('Combination of app label and model name seperated by a dot')
|
183
|
+
)
|
184
|
+
|
185
|
+
ignore_errors = models.BooleanField(
|
186
|
+
verbose_name=_('Ignore Errors'),
|
187
|
+
default=False,
|
188
|
+
help_text=_('If set, exceptions during log creation will be ignored')
|
189
|
+
)
|
190
|
+
|
191
|
+
exclude_fields = models.BooleanField(
|
192
|
+
verbose_name=_('Exclude Fields'),
|
193
|
+
default=False,
|
194
|
+
help_text=_(
|
195
|
+
'If set, Log Configuration Fields will be excluded from logging. '
|
196
|
+
'Otherwise only configured fields will be logged.'
|
197
|
+
)
|
198
|
+
)
|
199
|
+
|
200
|
+
|
201
|
+
class LogConfigField(models.Model):
|
202
|
+
|
203
|
+
class Meta:
|
204
|
+
verbose_name = _('Log Configuration Field')
|
205
|
+
verbose_name_plural = _('Log Configuration Fields')
|
206
|
+
db_table = 'accrete_log_config_field'
|
207
|
+
ordering = [Lower('field_name')]
|
208
|
+
indexes = [
|
209
|
+
models.Index(fields=['log_config'])
|
210
|
+
]
|
211
|
+
constraints = [
|
212
|
+
models.UniqueConstraint(
|
213
|
+
name='unique_config_field_name',
|
214
|
+
fields=('log_config', 'field_name')
|
215
|
+
)
|
216
|
+
]
|
217
|
+
|
218
|
+
log_config = models.ForeignKey(
|
219
|
+
verbose_name=_('Log Configuration'),
|
220
|
+
to='log.LogConfig',
|
221
|
+
related_name='fields',
|
222
|
+
on_delete=models.CASCADE
|
223
|
+
)
|
224
|
+
|
225
|
+
field_name = models.CharField(
|
226
|
+
verbose_name=_('Field Name'),
|
227
|
+
max_length=255,
|
228
|
+
help_text=_('Name of the field to log')
|
229
|
+
)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from .models import Log
|
2
|
+
from accrete.tenant import get_tenant
|
3
|
+
|
4
|
+
|
5
|
+
def current_log_state(model: str, object_id: int):
|
6
|
+
tenant = get_tenant()
|
7
|
+
tenant_id = tenant and tenant.id or None
|
8
|
+
logs = Log.objects.raw("""
|
9
|
+
SELECT
|
10
|
+
log_value.id,
|
11
|
+
log_value.model,
|
12
|
+
log_value.field,
|
13
|
+
log_value.object_id,
|
14
|
+
log_value.log_date,
|
15
|
+
log_value.old_value_type,
|
16
|
+
log_value.new_value_type,
|
17
|
+
log_value.old_value,
|
18
|
+
log_value.new_value
|
19
|
+
FROM (
|
20
|
+
SELECT model, field, object_id, MAX(log_date) AS log_date
|
21
|
+
FROM accrete_log
|
22
|
+
WHERE
|
23
|
+
model = %s AND
|
24
|
+
object_id = %s AND
|
25
|
+
tenant_id = %s
|
26
|
+
GROUP BY model, field, object_id
|
27
|
+
) log_result
|
28
|
+
JOIN accrete_log log_value ON
|
29
|
+
log_value.model = log_result.model AND
|
30
|
+
log_value.field = log_result.field AND
|
31
|
+
log_value.object_id = log_result.object_id AND
|
32
|
+
log_result.log_date = log_value.log_date;
|
33
|
+
""", [model, object_id, tenant_id])
|
34
|
+
return logs
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
from django.db import models
|
4
|
+
|
5
|
+
from accrete.tenant import get_member, get_tenant
|
6
|
+
from accrete.utils.log import log_time
|
7
|
+
from .models import Log, LogConfig
|
8
|
+
from . import helper, queries
|
9
|
+
|
10
|
+
_logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
@log_time
|
14
|
+
def create_log(sender, **kwargs):
|
15
|
+
|
16
|
+
def _create_log():
|
17
|
+
member = get_member()
|
18
|
+
user = member and member.user or None
|
19
|
+
object_id = instance.id
|
20
|
+
log_rows = queries.current_log_state(model, object_id)
|
21
|
+
log_state, log_info = helper.log_state_to_dict(log_rows)
|
22
|
+
instance_state = helper.get_instance_state(instance)
|
23
|
+
diff = dict(log_state.items() ^ instance_state.items())
|
24
|
+
logs_to_create = []
|
25
|
+
for field, value in instance_state.items():
|
26
|
+
if field not in diff.keys():
|
27
|
+
continue
|
28
|
+
value_type = helper.internal_type_to_log_type(instance._meta.get_field(field))
|
29
|
+
if value_type is None:
|
30
|
+
continue
|
31
|
+
if value is None:
|
32
|
+
new_value = None
|
33
|
+
else:
|
34
|
+
new_value = str(value)
|
35
|
+
log = Log(
|
36
|
+
model=model,
|
37
|
+
field=field,
|
38
|
+
object_id=instance.id,
|
39
|
+
old_value_type=log_info.get(field, {}).get('new_value_type', ''),
|
40
|
+
old_value=log_state.get(field),
|
41
|
+
new_value_type=helper.internal_type_to_log_type(instance._meta.get_field(field)),
|
42
|
+
new_value=new_value,
|
43
|
+
user=user,
|
44
|
+
tenant=get_tenant()
|
45
|
+
)
|
46
|
+
logs_to_create.append(log)
|
47
|
+
Log.objects.bulk_create(logs_to_create)
|
48
|
+
|
49
|
+
instance: models.Model = kwargs.get('instance')
|
50
|
+
model = f'{instance._meta.app_label}.{instance._meta.model_name}'
|
51
|
+
log_config = LogConfig.objects.filter(model=model).first()
|
52
|
+
if not log_config:
|
53
|
+
return
|
54
|
+
|
55
|
+
try:
|
56
|
+
_create_log()
|
57
|
+
except Exception as e:
|
58
|
+
_logger.exception(e)
|
59
|
+
if log_config.ignore_errors:
|
60
|
+
pass
|
61
|
+
else:
|
62
|
+
raise e
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Generated by Django 5.1.1 on 2025-01-07 20:24
|
2
|
+
|
3
|
+
import django.db.models.deletion
|
4
|
+
from django.db import migrations, models
|
5
|
+
|
6
|
+
|
7
|
+
class Migration(migrations.Migration):
|
8
|
+
|
9
|
+
dependencies = [
|
10
|
+
('accrete', '0002_initial'),
|
11
|
+
('sequence', '0002_alter_sequence_name'),
|
12
|
+
]
|
13
|
+
|
14
|
+
operations = [
|
15
|
+
migrations.AlterField(
|
16
|
+
model_name='sequence',
|
17
|
+
name='tenant',
|
18
|
+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accrete.tenant', verbose_name='Tenant'),
|
19
|
+
),
|
20
|
+
]
|
@@ -0,0 +1,11 @@
|
|
1
|
+
{% load i18n %}
|
2
|
+
|
3
|
+
{% if form.non_field_errors or form.save_error %}
|
4
|
+
<div class="notification is-danger is-light">
|
5
|
+
{{ form.non_field_errors }}
|
6
|
+
{% if form.save_error %}
|
7
|
+
<p>{{ form.save_error }}</p>
|
8
|
+
<span>{% translate 'Error ID' %}: {{ form.save_error_id }}</span>
|
9
|
+
{% endif %}
|
10
|
+
</div>
|
11
|
+
{% endif %}
|
accrete/managers.py
CHANGED
@@ -40,3 +40,10 @@ class TenantManager(models.Manager):
|
|
40
40
|
update_conflicts=update_conflicts, update_fields=update_fields,
|
41
41
|
unique_fields=unique_fields
|
42
42
|
)
|
43
|
+
|
44
|
+
|
45
|
+
class MemberManager(TenantManager):
|
46
|
+
|
47
|
+
def get_queryset(self):
|
48
|
+
queryset = super().get_queryset().select_related('tenant', 'user')
|
49
|
+
return queryset
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Generated by Django 5.1.1 on 2025-01-10 07:57
|
2
|
+
|
3
|
+
from django.db import migrations
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('accrete', '0002_initial'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.RemoveField(
|
14
|
+
model_name='member',
|
15
|
+
name='name',
|
16
|
+
),
|
17
|
+
]
|
accrete/models.py
CHANGED
@@ -3,12 +3,10 @@ from django.conf import settings
|
|
3
3
|
from django.utils.translation import gettext_lazy as _
|
4
4
|
from django.contrib.auth.validators import UnicodeUsernameValidator
|
5
5
|
from accrete.tenant import get_tenant
|
6
|
-
from accrete.
|
7
|
-
from accrete.managers import TenantManager
|
8
|
-
from accrete.tenant import unscoped
|
6
|
+
from accrete.managers import TenantManager, MemberManager
|
9
7
|
|
10
8
|
|
11
|
-
class TenantModel(models.Model
|
9
|
+
class TenantModel(models.Model):
|
12
10
|
|
13
11
|
class Meta:
|
14
12
|
abstract = True
|
@@ -88,8 +86,6 @@ class Member(models.Model):
|
|
88
86
|
ordering = ['tenant', 'user']
|
89
87
|
db_table = 'accrete_member'
|
90
88
|
|
91
|
-
username_validator = UnicodeUsernameValidator()
|
92
|
-
|
93
89
|
user = models.ForeignKey(
|
94
90
|
to=settings.AUTH_USER_MODEL,
|
95
91
|
related_name='memberships',
|
@@ -102,18 +98,6 @@ class Member(models.Model):
|
|
102
98
|
on_delete=models.CASCADE
|
103
99
|
)
|
104
100
|
|
105
|
-
name = models.CharField(
|
106
|
-
verbose_name=_('Name'),
|
107
|
-
max_length=150,
|
108
|
-
help_text=_(
|
109
|
-
'150 characters or fewer.'
|
110
|
-
'Letters, digits and @/./+/-/_ only.'
|
111
|
-
),
|
112
|
-
blank=True,
|
113
|
-
null=True,
|
114
|
-
validators=[username_validator],
|
115
|
-
)
|
116
|
-
|
117
101
|
is_active = models.BooleanField(
|
118
102
|
verbose_name=_('Active'),
|
119
103
|
default=True
|
@@ -125,10 +109,10 @@ class Member(models.Model):
|
|
125
109
|
through_fields=('member', 'access_group')
|
126
110
|
)
|
127
111
|
|
128
|
-
objects =
|
112
|
+
objects = MemberManager()
|
129
113
|
|
130
114
|
def __str__(self):
|
131
|
-
return f'{self.
|
115
|
+
return f'{self.user}'
|
132
116
|
|
133
117
|
|
134
118
|
class AccessGroup(models.Model):
|
@@ -1,18 +1,32 @@
|
|
1
1
|
accrete/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
2
|
accrete/admin.py,sha256=MUYUmCFlGYPowiXTbwl4_Q6Cq0-neiL53WW4P76JCLs,1174
|
3
|
-
accrete/annotation.py,sha256=GCpTTX3fJH9w2vOOOydKcAALdEd1gRNjh_Ws9LkJ4aA,1355
|
4
3
|
accrete/apps.py,sha256=F7ynMLHJr_6bRujWtZVUzCliY2CGKiDvyUmL4F68L2E,146
|
5
4
|
accrete/config.py,sha256=eJUbvyBO3DvAD6xkVKjTAzlXy7V7EK9bVyb91girfUs,299
|
6
5
|
accrete/forms.py,sha256=H2hPQemslRLvTVV0Wl1TfUmTc5wU3Z98nQTMiLMliqo,1288
|
7
|
-
accrete/managers.py,sha256=
|
6
|
+
accrete/managers.py,sha256=DevRVm7cStvlfz6TriitSINr40POCi4HNaHX48VkrMA,1620
|
8
7
|
accrete/middleware.py,sha256=YN73WloNkN01oel9Dcj3xyhurcWoB6zMV0NT3hY8DGw,2264
|
9
|
-
accrete/models.py,sha256=
|
8
|
+
accrete/models.py,sha256=k6BLNeQY4H_-WiODLpMoNJp_g3Dm4sTfeNj5n68oJ88,5109
|
10
9
|
accrete/storage.py,sha256=Jp3oE_uPMqgarjS_G49KDFrR2eSe4XuIJK9oAF_QBxk,1288
|
11
10
|
accrete/tenant.py,sha256=-__rFtYrc5SbxFX2-Dvw6Wc1XzSAD6DeIBRuOVQlbWQ,1908
|
12
11
|
accrete/tests.py,sha256=Agltbzwwh5htvq_Qi9vqvxutzmg_GwgPS_N19xJZRlw,7197
|
13
12
|
accrete/urls.py,sha256=goDFR-yhOlLLy7AMi9pmh2aBkxdtZtwXNg6mwI2zPhU,227
|
14
13
|
accrete/views.py,sha256=dwgXLkIjqjG16vlG4C6QeS191HoDnT7bZtkLahJB6wQ,2582
|
15
14
|
accrete/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
+
accrete/contrib/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
+
accrete/contrib/log/admin.py,sha256=a3VCS2_YmahBShewMS9yn1QOln-3rmCJqMsa6VnNqdU,1151
|
17
|
+
accrete/contrib/log/apps.py,sha256=O0Cje3MmpxPToJVgO195lBg0tRCy9Ou87-ntcdGBKM0,369
|
18
|
+
accrete/contrib/log/helper.py,sha256=Zzmwxz7vP0jdW2LCw8gDpPNBkqlELaCydz-nxGh7F5Y,2493
|
19
|
+
accrete/contrib/log/models.py,sha256=PUGtm2mmZIhWf7Idpu9hZ-1mJtKU2p975e7eoCZ2cHE,6082
|
20
|
+
accrete/contrib/log/queries.py,sha256=JMI_q6dQ0JjyORtfRPOW92of45TFxqPiwIhHEEar34o,1126
|
21
|
+
accrete/contrib/log/signals.py,sha256=Gsb7B9nRhLgQQ8qxtbCEnvgHkFK9v7JfYJfAupFQ21I,2036
|
22
|
+
accrete/contrib/log/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
|
23
|
+
accrete/contrib/log/views.py,sha256=xc1IQHrsij7j33TUbo-_oewy3vs03pw_etpBWaMYJl0,63
|
24
|
+
accrete/contrib/log/migrations/0001_initial.py,sha256=wkGMfaYc09dOuJXY0JengDBTkS98rMutUzmEAb6eK3E,2130
|
25
|
+
accrete/contrib/log/migrations/0002_log_user_alter_log_new_value_type_and_more.py,sha256=E3IUq4P62sapYzbKbUuufdCbN4O4kRphpcyVfUpJwrc,1346
|
26
|
+
accrete/contrib/log/migrations/0003_alter_log_tenant.py,sha256=yp3WizRdAPvPctMiCmKcsqVix3a0LaIRmuAB6LJ6Gb0,582
|
27
|
+
accrete/contrib/log/migrations/0004_logconfig_logconfigfield.py,sha256=iXD3kDj8GAD5Dce9xVozl0n8KCQaK4LsQkLHR3BDZ28,2367
|
28
|
+
accrete/contrib/log/migrations/0005_logconfig_exclude_fields_and_more.py,sha256=eN0AG2vR80-9NvMBfijjM04qPfRGxO7B8VFabrEZ_ZQ,1127
|
29
|
+
accrete/contrib/log/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
30
|
accrete/contrib/sequence/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
31
|
accrete/contrib/sequence/admin.py,sha256=mTjab5cVklRUIQcSrsUo-_JgtXEsSdcFj_gfWhlStS4,273
|
18
32
|
accrete/contrib/sequence/apps.py,sha256=2SalOz9piCrbOPudCh0grN1eojN9kEC4-jcNzBmfqEk,164
|
@@ -23,6 +37,7 @@ accrete/contrib/sequence/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6
|
|
23
37
|
accrete/contrib/sequence/views.py,sha256=xc1IQHrsij7j33TUbo-_oewy3vs03pw_etpBWaMYJl0,63
|
24
38
|
accrete/contrib/sequence/migrations/0001_initial.py,sha256=iAR_hhGN2wDAk40IS9PwEsm7iYqfgasoKRrTLFEpOY8,1352
|
25
39
|
accrete/contrib/sequence/migrations/0002_alter_sequence_name.py,sha256=70tpxJ0_edWpzj69PxV73m3CwI7Hy5x1YK8m6_2niP4,398
|
40
|
+
accrete/contrib/sequence/migrations/0003_alter_sequence_tenant.py,sha256=W0RJKpTK1DKKpipktRJASr9aG-24Xuzr7GxYYGkwoJk,535
|
26
41
|
accrete/contrib/sequence/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
27
42
|
accrete/contrib/system_mail/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
28
43
|
accrete/contrib/system_mail/admin.py,sha256=9hXwwfZn446juyRoBOygLWm12X6N9waRC-1LHBLrgZk,227
|
@@ -303,6 +318,7 @@ accrete/contrib/ui/templates/django/forms/widgets/select.html,sha256=uSfDpOQox2m
|
|
303
318
|
accrete/contrib/ui/templates/django/forms/widgets/text.html,sha256=MSmLlQc7PsPoDLVtTOOiWNprrsPriNr712yFxaHyDIo,47
|
304
319
|
accrete/contrib/ui/templates/django/forms/widgets/textarea.html,sha256=c9BTedqb3IkXLyVYd0p9pR8DFnsXCNGoxVBWZTk_Fic,278
|
305
320
|
accrete/contrib/ui/templates/ui/content_right.html,sha256=XUF1tYpSKfO9FleYtJ2QmWPmwdLYxLHXdBLRa-BrFUs,221
|
321
|
+
accrete/contrib/ui/templates/ui/form_error.html,sha256=uA8FLdZyeU0vXJHlGK3rcBqcmXb63MLPV32uQyUTak4,348
|
306
322
|
accrete/contrib/ui/templates/ui/layout.html,sha256=PVqvIPYdWPX8IuAes1db0SWlw05xu0SNp8vmV9n1Sms,12889
|
307
323
|
accrete/contrib/ui/templates/ui/list.html,sha256=NY8DmHGl3n5O1u-_B9a_mlAck19ZmpYthzecADuc3BM,2250
|
308
324
|
accrete/contrib/ui/templates/ui/list_update.html,sha256=mLQTCgkKfVI5jrgei-Upc1u87iXL0Q63uLzXHPwMyeo,110
|
@@ -356,6 +372,7 @@ accrete/contrib/user_registration/templates/user_registration/registration.html,
|
|
356
372
|
accrete/contrib/user_registration/templates/user_registration/mail_templates/confirmation_mail.html,sha256=5UkpGUrDAazrr_gKguOnOykr77a2FLgD2gnvUxzHfyg,192
|
357
373
|
accrete/migrations/0001_initial.py,sha256=azThbc8otEhxJwo8BIgOt5eC30mxXhKJLBAazZFe3BA,4166
|
358
374
|
accrete/migrations/0002_initial.py,sha256=dFOM7kdHlx7pVAh8cTDlZMtciN4O9Z547HAzEKnygZE,1628
|
375
|
+
accrete/migrations/0003_remove_member_name.py,sha256=bnZrzOIXcqsoGfbqgohTN5OHm2IldnLlBz1HNJDeqKc,315
|
359
376
|
accrete/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
360
377
|
accrete/utils/__init__.py,sha256=saw9zi2XItJOPbv4fjTXOpl7StNtC803jHhapFcGx08,312
|
361
378
|
accrete/utils/dates.py,sha256=apM6kt6JhGrKgoT0jfav1W-8AUVTxNc9xt3fJQ2n0JI,1492
|
@@ -363,7 +380,7 @@ accrete/utils/forms.py,sha256=IvxbXNpSd4a-JBgsTJhs2GHe-DCRWX-xnVPRcoiCzbI,3104
|
|
363
380
|
accrete/utils/log.py,sha256=BH0MBDweAjx30wGBO4F3sFhbgkSoEs7T1lLLjlYZNnA,407
|
364
381
|
accrete/utils/models.py,sha256=2xTacvcpmDK_Bp4rAK7JdVLf8HU009LYNJ6eSpMgYZI,1014
|
365
382
|
accrete/utils/views.py,sha256=AutijWetWGgjdO1PNc4gxCblT-i1fAfldNDFRbO9Sac,5012
|
366
|
-
accrete-0.0.
|
367
|
-
accrete-0.0.
|
368
|
-
accrete-0.0.
|
369
|
-
accrete-0.0.
|
383
|
+
accrete-0.0.113.dist-info/METADATA,sha256=kvqYem5bYx1j0oWAFyHESNnmukLdsBxfiu-kc-PDB-k,4953
|
384
|
+
accrete-0.0.113.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
385
|
+
accrete-0.0.113.dist-info/licenses/LICENSE,sha256=_7laeMIHnsd3Y2vJEXDYXq_PEXxIcjgJsGt8UIKTRWc,1057
|
386
|
+
accrete-0.0.113.dist-info/RECORD,,
|
accrete/annotation.py
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
from django.db.models import Field, Subquery, OuterRef
|
2
|
-
from django.db.models.expressions import Func
|
3
|
-
from django.db.models.aggregates import Aggregate
|
4
|
-
|
5
|
-
|
6
|
-
class Annotation:
|
7
|
-
|
8
|
-
def __init__(
|
9
|
-
self,
|
10
|
-
verbose_name: str,
|
11
|
-
field: type[Field],
|
12
|
-
function: type[Func] | type[Aggregate],
|
13
|
-
help_text: str = '',
|
14
|
-
**kwargs
|
15
|
-
):
|
16
|
-
self.verbose_name = verbose_name or self
|
17
|
-
self.field = field
|
18
|
-
self.function = function
|
19
|
-
self.help_text = help_text
|
20
|
-
self.__dict__.update(kwargs)
|
21
|
-
|
22
|
-
|
23
|
-
class AnnotationModelMixin:
|
24
|
-
|
25
|
-
@classmethod
|
26
|
-
def get_annotations(cls) -> list[dict]:
|
27
|
-
return list({'name': a, 'annotation': getattr(cls, a)} for a in filter(
|
28
|
-
lambda a:
|
29
|
-
not a.startswith('__')
|
30
|
-
and isinstance(getattr(cls, a), Annotation),
|
31
|
-
cls.__dict__
|
32
|
-
))
|
33
|
-
|
34
|
-
|
35
|
-
class AnnotationManagerMixin:
|
36
|
-
|
37
|
-
def get_annotations(self, queryset):
|
38
|
-
if not hasattr(self.model, 'get_annotations'):
|
39
|
-
return {}
|
40
|
-
return {
|
41
|
-
annotation['name']: Subquery(
|
42
|
-
queryset.annotate(**{
|
43
|
-
annotation['name']: annotation['annotation'].function
|
44
|
-
}).filter(pk=OuterRef('pk')).values(annotation['name'])[:1])
|
45
|
-
for annotation in self.model.get_annotations()
|
46
|
-
}
|
File without changes
|
File without changes
|