exceptbot-drf 1.0.0__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.
exceptbot/__init__.py ADDED
@@ -0,0 +1 @@
1
+ default_app_config = 'exceptbot.apps.ExceptBotConfig'
exceptbot/admin.py ADDED
@@ -0,0 +1,260 @@
1
+ from django.contrib import admin
2
+ from django.utils import timezone
3
+ from django.utils.html import format_html
4
+
5
+ from .models import ExceptionLog, AppSettings
6
+
7
+
8
+ @admin.register(ExceptionLog)
9
+ class ExceptionLogAdmin(admin.ModelAdmin):
10
+ list_display = (
11
+ 'id',
12
+ 'exception_type',
13
+ 'short_url',
14
+ 'http_method',
15
+ 'status_code',
16
+ 'source_badge',
17
+ 'file_name_short',
18
+ 'line_number',
19
+ 'user',
20
+ 'count',
21
+ 'is_resolved_badge',
22
+ 'timestamp',
23
+ )
24
+
25
+ list_filter = (
26
+ 'is_resolved',
27
+ 'source',
28
+ 'http_method',
29
+ 'status_code',
30
+ 'exception_type',
31
+ 'timestamp',
32
+ 'user',
33
+ )
34
+
35
+ search_fields = (
36
+ 'exception_type',
37
+ 'url_path',
38
+ 'file_name',
39
+ 'full_error_message',
40
+ 'error_line_content',
41
+ 'user__username',
42
+ 'user__email',
43
+ )
44
+
45
+ readonly_fields = (
46
+ 'url_path',
47
+ 'http_method',
48
+ 'status_code',
49
+ 'source',
50
+ 'request_data_display',
51
+ 'ip_address',
52
+ 'user_agent',
53
+ 'exception_type',
54
+ 'full_error_message_display',
55
+ 'file_name',
56
+ 'file_content_display',
57
+ 'line_number',
58
+ 'error_line_content',
59
+ 'user',
60
+ 'timestamp',
61
+ 'count',
62
+ 'resolved_at',
63
+ 'ai_suggestion_display',
64
+ )
65
+
66
+ ordering = ('-timestamp',)
67
+
68
+ list_per_page = 25
69
+
70
+ fieldsets = (
71
+ ('error info', {
72
+ 'fields': (
73
+ 'exception_type',
74
+ 'url_path',
75
+ 'http_method',
76
+ 'status_code',
77
+ 'source',
78
+ 'timestamp',
79
+ 'count',
80
+ )
81
+ }),
82
+ (' position of code', {
83
+ 'fields': (
84
+ 'file_name',
85
+ 'line_number',
86
+ 'error_line_content',
87
+ 'file_content_display',
88
+ )
89
+ }),
90
+ (' request information', {
91
+ 'fields': (
92
+ 'request_data_display',
93
+ 'ip_address',
94
+ 'user_agent',
95
+ ),
96
+ 'classes': ('collapse',),
97
+ }),
98
+ (' error message', {
99
+ 'fields': (
100
+ 'full_error_message_display',
101
+ ),
102
+ 'classes': ('collapse',),
103
+ }),
104
+ ('user and status resolved', {
105
+ 'fields': (
106
+ 'user',
107
+ 'is_resolved',
108
+ 'resolved_by',
109
+ 'resolved_at',
110
+ 'resolution_note',
111
+ )
112
+ }),
113
+ ('suggestion of ai', {
114
+ 'fields': (
115
+ 'ai_suggestion_display',
116
+ ),
117
+ 'classes': ('collapse',),
118
+ }),
119
+ )
120
+
121
+ # actions = ['mark_as_resolved', 'mark_as_unresolved']
122
+
123
+ @admin.display(description='URL')
124
+ def short_url(self, obj):
125
+ if len(obj.url_path) > 40:
126
+ return obj.url_path[:40] + '...'
127
+ return obj.url_path
128
+
129
+ @admin.display(description='File')
130
+ def file_name_short(self, obj):
131
+ if not obj.file_name:
132
+ return '-'
133
+ return obj.file_name.split('/')[-1].split('\\')[-1]
134
+
135
+ @admin.display(description='Source')
136
+ def source_badge(self, obj):
137
+ colors = {
138
+ 'backend': '#007bff',
139
+ 'frontend': '#28a745',
140
+ 'unknown': '#6c757d',
141
+ }
142
+ color = colors.get(obj.source, '#6c757d')
143
+ return format_html(
144
+ '<span style="background:{};color:white;padding:2px 8px;'
145
+ 'border-radius:4px;font-size:11px;">{}</span>',
146
+ color,
147
+ obj.get_source_display(),
148
+ )
149
+
150
+ @admin.display(description='وضعیت', boolean=True)
151
+ def is_resolved_badge(self, obj):
152
+ return obj.is_resolved
153
+
154
+ @admin.display(description='Request Data')
155
+ def request_data_display(self, obj):
156
+ if not obj.request_data:
157
+ return '-'
158
+ import json
159
+ pretty = json.dumps(obj.request_data, indent=2, ensure_ascii=False)
160
+ return format_html(
161
+ '<pre style="background:#f8f9fa;padding:10px;'
162
+ 'border-radius:4px;max-height:400px;overflow:auto;">{}</pre>',
163
+ pretty,
164
+ )
165
+
166
+ @admin.display(description='Full Error Message')
167
+ def full_error_message_display(self, obj):
168
+ if not obj.full_error_message:
169
+ return '-'
170
+ return format_html(
171
+ '<pre style="background:#fff3cd;padding:10px;'
172
+ 'border-radius:4px;max-height:400px;overflow:auto;'
173
+ 'white-space:pre-wrap;">{}</pre>',
174
+ obj.full_error_message,
175
+ )
176
+
177
+ @admin.display(description='File Content')
178
+ def file_content_display(self, obj):
179
+ if not obj.file_content:
180
+ return '-'
181
+ return format_html(
182
+ '<pre style="background:#f8f9fa;padding:10px;'
183
+ 'border-radius:4px;max-height:500px;overflow:auto;'
184
+ 'white-space:pre-wrap;">{}</pre>',
185
+ obj.file_content,
186
+ )
187
+
188
+ @admin.display(description='AI Suggestion')
189
+ def ai_suggestion_display(self, obj):
190
+ if not obj.ai_suggestion:
191
+ return 'هنوز پیشنهادی ساخته نشده است.'
192
+ return format_html(
193
+ '<div style="background:#e7f3ff;padding:15px;'
194
+ 'border-radius:4px;max-height:600px;overflow:auto;'
195
+ 'white-space:pre-wrap;">{}</div>',
196
+ obj.ai_suggestion,
197
+ )
198
+
199
+ @admin.action(description='✅ علامت‌گذاری به‌عنوان رفع‌شده')
200
+ def mark_as_resolved(self, request, queryset):
201
+ updated = queryset.update(
202
+ is_resolved=True,
203
+ resolved_by=request.user,
204
+ resolved_at=timezone.now(),
205
+ )
206
+ self.message_user(
207
+ request,
208
+ f'{updated} خطا به‌عنوان رفع‌شده علامت‌گذاری شد.'
209
+ )
210
+
211
+ @admin.action(description='↩️ برگرداندن به حالت رفع‌نشده')
212
+ def mark_as_unresolved(self, request, queryset):
213
+ updated = queryset.update(
214
+ is_resolved=False,
215
+ resolved_by=None,
216
+ resolved_at=None,
217
+ resolution_note='',
218
+ )
219
+ self.message_user(
220
+ request,
221
+ f'{updated} خطا به حالت رفع‌نشده برگشت.'
222
+ )
223
+
224
+ def has_add_permission(self, request):
225
+ return False
226
+
227
+ def has_change_permission(self, request, obj=None):
228
+ return True
229
+
230
+
231
+ @admin.register(AppSettings)
232
+ class AppSettingsAdmin(admin.ModelAdmin):
233
+ list_display = (
234
+ 'id',
235
+ 'project_name',
236
+ 'base_url',
237
+ 'has_openai_key',
238
+ )
239
+
240
+ fieldsets = (
241
+ ('اطلاعات پروژه', {
242
+ 'fields': ('project_name', 'base_url'),
243
+ }),
244
+ ('تنظیمات هوش مصنوعی', {
245
+ 'fields': ('openai_api_key',),
246
+ 'description': 'برای دریافت پیشنهاد AI، کلید OpenAI خود را وارد کنید.',
247
+ }),
248
+ )
249
+
250
+ @admin.display(description='OpenAI Key', boolean=True)
251
+ def has_openai_key(self, obj):
252
+ return bool(obj.openai_api_key)
253
+
254
+ def has_add_permission(self, request):
255
+ if AppSettings.objects.exists():
256
+ return False
257
+ return super().has_add_permission(request)
258
+
259
+ def has_delete_permission(self, request, obj=None):
260
+ return False
exceptbot/apps.py ADDED
@@ -0,0 +1,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class ExceptBotConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'exceptbot'
7
+ verbose_name = "ExceptBot: Exception Logger with AI Suggestions"
@@ -0,0 +1,126 @@
1
+ import sys
2
+ import traceback
3
+
4
+ from django.db import transaction
5
+ from django.utils import timezone
6
+ from .models import ExceptionLog, AppSettings
7
+ SENSITIVE_FIELDS = {
8
+ 'password', 'password1', 'password2',
9
+ 'token', 'access', 'refresh',
10
+ 'secret', 'api_key', 'authorization',
11
+ }
12
+
13
+ def mask_sensitive_data(data):
14
+ if not isinstance(data, dict):
15
+ return data
16
+
17
+ masked = {}
18
+ for key, value in data.items():
19
+ if key.lower() in SENSITIVE_FIELDS:
20
+ masked[key] = '***MASKED***'
21
+ elif isinstance(value, dict):
22
+ masked[key] = mask_sensitive_data(value)
23
+ elif isinstance(value, list):
24
+ masked[key] = [
25
+ mask_sensitive_data(item) if isinstance(item, dict) else item
26
+ for item in value
27
+ ]
28
+ else:
29
+ masked[key] = value
30
+ return masked
31
+
32
+
33
+ class ExceptBotMiddleware:
34
+ def __init__(self, get_response):
35
+ self.get_response = get_response
36
+
37
+ def __call__(self, request):
38
+ response = self.get_response(request)
39
+ return response
40
+
41
+ def process_exception(self, request, exception):
42
+ user = None
43
+ if hasattr(request, 'user') and request.user.is_authenticated:
44
+ user = request.user
45
+ app_settings = AppSettings.get_solo()
46
+ project_name = app_settings.project_name or ''
47
+ exc_type, exc_value, exc_traceback = sys.exc_info()
48
+ formatted_traceback = traceback.extract_tb(exc_traceback)
49
+
50
+ file_name = None
51
+ line_number = None
52
+ error_line_content = None
53
+ if project_name:
54
+ for stack in reversed(formatted_traceback):
55
+ if project_name in stack.filename:
56
+ file_name = stack.filename
57
+ line_number = stack.lineno
58
+ error_line_content = stack.line
59
+ break
60
+ if not file_name and formatted_traceback:
61
+ stack = formatted_traceback[-1]
62
+ file_name = stack.filename
63
+ line_number = stack.lineno
64
+ error_line_content = stack.line
65
+
66
+ full_error_message = ''.join(
67
+ traceback.format_exception(exc_type, exc_value, exc_traceback)
68
+ )
69
+ file_content = ''
70
+ try:
71
+ with open(file_name, 'r', encoding='utf-8') as file:
72
+ file_content = file.read()
73
+ except (FileNotFoundError, PermissionError, UnicodeDecodeError, TypeError):
74
+ file_content = f"# Could not read file: {file_name}"
75
+ url_path = request.path
76
+ exception_type = str(type(exception).__name__)
77
+ http_method = request.method
78
+ status_code = 500
79
+ source = request.META.get('HTTP_X_CLIENT_TYPE', 'backend').lower()
80
+ if source not in ('backend', 'frontend'):
81
+ source = 'unknown'
82
+ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
83
+ if x_forwarded_for:
84
+ ip_address = x_forwarded_for.split(',')[0].strip()
85
+ else:
86
+ ip_address = request.META.get('REMOTE_ADDR')
87
+ user_agent = request.META.get('HTTP_USER_AGENT', '')[:1000]
88
+
89
+ request_data = None
90
+ try:
91
+ if hasattr(request, 'data'):
92
+ request_data = mask_sensitive_data(dict(request.data))
93
+ elif http_method in ('POST', 'PUT', 'PATCH'):
94
+ request_data = mask_sensitive_data(dict(request.POST))
95
+ except Exception:
96
+ request_data = None
97
+ with transaction.atomic():
98
+ existing_exception = ExceptionLog.objects.filter(
99
+ exception_type=exception_type,
100
+ file_name=file_name,
101
+ is_resolved=False,
102
+ ).first()
103
+
104
+ if existing_exception:
105
+ existing_exception.count += 1
106
+ existing_exception.timestamp = timezone.now()
107
+ existing_exception.save(update_fields=['count', 'timestamp'])
108
+ else:
109
+ ExceptionLog.objects.create(
110
+ url_path=url_path,
111
+ exception_type=exception_type,
112
+ full_error_message=full_error_message,
113
+ file_name=file_name,
114
+ file_content=file_content,
115
+ line_number=line_number,
116
+ error_line_content=error_line_content,
117
+ user=user,
118
+ http_method=http_method,
119
+ status_code=status_code,
120
+ source=source,
121
+ ip_address=ip_address,
122
+ user_agent=user_agent,
123
+ request_data=request_data,
124
+ )
125
+
126
+ return None
@@ -0,0 +1,63 @@
1
+ # Generated by Django 6.1.1 on 2026-09-16 08:05
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
+ initial = True
11
+
12
+ dependencies = [
13
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name='AppSettings',
19
+ fields=[
20
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
+ ('openai_api_key', models.CharField(blank=True, help_text="Required for AI recommendations. Get an OpenAI account; google 'openai api key'", max_length=255, null=True)),
22
+ ('base_url', models.CharField(blank=True, help_text='ex: https://exceptbot.com', max_length=255, null=True)),
23
+ ('project_name', models.CharField(blank=True, help_text="The directory name of your project. Ex: 'exceptbot'", max_length=255, null=True)),
24
+ ],
25
+ options={
26
+ 'verbose_name': 'App Setting',
27
+ 'verbose_name_plural': 'App Settings',
28
+ },
29
+ ),
30
+ migrations.CreateModel(
31
+ name='ExceptionLog',
32
+ fields=[
33
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
34
+ ('exception_type', models.CharField(db_index=True, max_length=255)),
35
+ ('full_error_message', models.TextField(blank=True)),
36
+ ('file_name', models.CharField(db_index=True, max_length=500)),
37
+ ('file_content', models.TextField(blank=True)),
38
+ ('line_number', models.PositiveIntegerField(blank=True, null=True)),
39
+ ('error_line_content', models.TextField(blank=True)),
40
+ ('url_path', models.TextField(db_index=True)),
41
+ ('http_method', models.CharField(db_index=True, default='GET', max_length=10)),
42
+ ('status_code', models.PositiveIntegerField(blank=True, db_index=True, null=True)),
43
+ ('source', models.CharField(choices=[('backend', 'Backend'), ('frontend', 'Frontend'), ('unknown', 'Unknown')], db_index=True, default='backend', max_length=20)),
44
+ ('request_data', models.JSONField(blank=True, null=True)),
45
+ ('ip_address', models.GenericIPAddressField(blank=True, null=True)),
46
+ ('user_agent', models.TextField(blank=True)),
47
+ ('timestamp', models.DateTimeField(auto_now_add=True, db_index=True)),
48
+ ('is_resolved', models.BooleanField(db_index=True, default=False)),
49
+ ('resolved_at', models.DateTimeField(blank=True, null=True)),
50
+ ('resolution_note', models.TextField(blank=True, default='')),
51
+ ('ai_suggestion', models.TextField(blank=True, null=True)),
52
+ ('count', models.PositiveIntegerField(default=1)),
53
+ ('resolved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resolved_exceptions', to=settings.AUTH_USER_MODEL)),
54
+ ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='caused_exceptions', to=settings.AUTH_USER_MODEL)),
55
+ ],
56
+ options={
57
+ 'verbose_name': 'Exception Log',
58
+ 'verbose_name_plural': 'Exception Logs',
59
+ 'ordering': ['-timestamp'],
60
+ 'indexes': [models.Index(fields=['is_resolved', '-timestamp'], name='exceptbot_e_is_reso_f0a0d5_idx'), models.Index(fields=['exception_type', 'file_name', 'is_resolved'], name='exceptbot_e_excepti_0cf5b5_idx'), models.Index(fields=['source', 'is_resolved'], name='exceptbot_e_source_e78949_idx')],
61
+ },
62
+ ),
63
+ ]
File without changes
exceptbot/models.py ADDED
@@ -0,0 +1,134 @@
1
+ import re
2
+
3
+ from django.conf import settings
4
+ from django.db import models
5
+ from django.utils import timezone
6
+
7
+
8
+ class ExceptionLog(models.Model):
9
+ SOURCE_CHOICES = [
10
+ ('backend', 'Backend'),
11
+ ('frontend', 'Frontend'),
12
+ ('unknown', 'Unknown'),
13
+ ]
14
+ exception_type = models.CharField(max_length=255, db_index=True)
15
+ full_error_message = models.TextField(blank=True)
16
+ file_name = models.CharField(max_length=500, db_index=True)
17
+ file_content = models.TextField(blank=True)
18
+ line_number = models.PositiveIntegerField(null=True, blank=True)
19
+ error_line_content = models.TextField(blank=True)
20
+ url_path = models.TextField(db_index=True)
21
+ http_method = models.CharField(max_length=10, default='GET', db_index=True)
22
+ status_code = models.PositiveIntegerField(null=True, blank=True, db_index=True)
23
+ source = models.CharField(
24
+ max_length=20,
25
+ choices=SOURCE_CHOICES,
26
+ default='backend',
27
+ db_index=True,
28
+ )
29
+ request_data = models.JSONField(null=True, blank=True)
30
+ ip_address = models.GenericIPAddressField(null=True, blank=True)
31
+ user_agent = models.TextField(blank=True)
32
+ user = models.ForeignKey(
33
+ settings.AUTH_USER_MODEL,
34
+ related_name='caused_exceptions',
35
+ null=True, blank=True,
36
+ on_delete=models.SET_NULL,
37
+ db_index=True,
38
+ )
39
+ timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
40
+ is_resolved = models.BooleanField(default=False, db_index=True)
41
+ resolved_by = models.ForeignKey(
42
+ settings.AUTH_USER_MODEL,
43
+ related_name='resolved_exceptions',
44
+ null=True, blank=True,
45
+ on_delete=models.SET_NULL,
46
+ )
47
+ resolved_at = models.DateTimeField(null=True, blank=True)
48
+ resolution_note = models.TextField(blank=True, default='')
49
+ ai_suggestion = models.TextField(null=True, blank=True)
50
+ count = models.PositiveIntegerField(default=1)
51
+
52
+ class Meta:
53
+ ordering = ['-timestamp']
54
+ verbose_name = 'Exception Log'
55
+ verbose_name_plural = 'Exception Logs'
56
+ indexes = [
57
+ models.Index(fields=['is_resolved', '-timestamp']),
58
+ models.Index(fields=['exception_type', 'file_name', 'is_resolved']),
59
+ models.Index(fields=['source', 'is_resolved']),
60
+ ]
61
+
62
+ def __str__(self):
63
+ return f"[{self.source}] {self.exception_type} at {self.url_path}"
64
+
65
+ def mark_resolved(self, user, note=''):
66
+ self.is_resolved = True
67
+ self.resolved_by = user
68
+ self.resolved_at = timezone.now()
69
+ self.resolution_note = note
70
+ self.save()
71
+
72
+ def get_blocks(self):
73
+ if not self.ai_suggestion:
74
+ return []
75
+ blocks = re.split(r'(```[a-zA-Z]*\n)', self.ai_suggestion)
76
+ formatted_blocks = []
77
+ in_code_block = False
78
+ language = None
79
+ for block in blocks:
80
+ if block.startswith('```'):
81
+ in_code_block = not in_code_block
82
+ language = block[3:].strip()
83
+ if language == '':
84
+ language = None
85
+ elif in_code_block:
86
+ formatted_blocks.append({
87
+ 'text': block.strip(),
88
+ 'is_code': True,
89
+ 'language': language,
90
+ })
91
+ else:
92
+ formatted_blocks.append({
93
+ 'text': block,
94
+ 'is_code': False,
95
+ 'language': None,
96
+ })
97
+ return formatted_blocks
98
+
99
+
100
+ class AppSettings(models.Model):
101
+ openai_api_key = models.CharField(
102
+ max_length=255,
103
+ blank=True,
104
+ null=True,
105
+ help_text="Required for AI recommendations. Get an OpenAI account; google 'openai api key'",
106
+ )
107
+ base_url = models.CharField(
108
+ max_length=255,
109
+ blank=True,
110
+ null=True,
111
+ help_text="ex: https://exceptbot.com",
112
+ )
113
+ project_name = models.CharField(
114
+ max_length=255,
115
+ blank=True,
116
+ null=True,
117
+ help_text="The directory name of your project. Ex: 'exceptbot'",
118
+ )
119
+
120
+ class Meta:
121
+ verbose_name = 'App Setting'
122
+ verbose_name_plural = 'App Settings'
123
+
124
+ def __str__(self):
125
+ return self.project_name or 'App Settings'
126
+
127
+ def save(self, *args, **kwargs):
128
+ self.pk = 1
129
+ super().save(*args, **kwargs)
130
+
131
+ @classmethod
132
+ def get_solo(cls):
133
+ obj, _ = cls.objects.get_or_create(pk=1)
134
+ return obj
@@ -0,0 +1,11 @@
1
+ from rest_framework.permissions import BasePermission
2
+
3
+ class IsSuperUser(BasePermission):
4
+ message = 'Only superusers can access ExceptBot endpoints.'
5
+
6
+ def has_permission(self, request, view):
7
+ return bool(
8
+ request.user
9
+ and request.user.is_authenticated
10
+ and request.user.is_superuser
11
+ )
@@ -0,0 +1,65 @@
1
+ from rest_framework import serializers
2
+ from .models import ExceptionLog, AppSettings
3
+
4
+
5
+ class ExceptionLogListSerializer(serializers.ModelSerializer):
6
+ user = serializers.StringRelatedField(read_only=True)
7
+ resolved_by = serializers.StringRelatedField(read_only=True)
8
+ source_display = serializers.SerializerMethodField()
9
+
10
+ class Meta:
11
+ model = ExceptionLog
12
+ fields = [
13
+ 'id',
14
+ 'exception_type',
15
+ 'url_path',
16
+ 'http_method',
17
+ 'status_code',
18
+ 'source',
19
+ 'source_display',
20
+ 'file_name',
21
+ 'line_number',
22
+ 'user',
23
+ 'count',
24
+ 'is_resolved',
25
+ 'resolved_by',
26
+ 'resolved_at',
27
+ 'timestamp',
28
+ ]
29
+
30
+ def get_source_display(self, obj):
31
+ return obj.get_source_display()
32
+
33
+
34
+
35
+ class ExceptionLogDetailSerializer(serializers.ModelSerializer):
36
+ user = serializers.StringRelatedField(read_only=True)
37
+ resolved_by = serializers.StringRelatedField(read_only=True)
38
+ source_display = serializers.SerializerMethodField()
39
+
40
+ class Meta:
41
+ model = ExceptionLog
42
+ fields = '__all__'
43
+
44
+ def get_source_display(self, obj):
45
+ return obj.get_source_display()
46
+
47
+
48
+
49
+ class ExceptionLogResolveSerializer(serializers.Serializer):
50
+ resolution_note = serializers.CharField(
51
+ required=False,
52
+ allow_blank=True,
53
+ default='',
54
+ max_length=2000,
55
+ )
56
+
57
+
58
+
59
+ class AppSettingsSerializer(serializers.ModelSerializer):
60
+ class Meta:
61
+ model = AppSettings
62
+ fields = ['openai_api_key', 'base_url', 'project_name']
63
+ extra_kwargs = {
64
+ 'openai_api_key': {'write_only': False, 'required': False},
65
+ }
exceptbot/tests.py ADDED
File without changes