66from django import forms
77from django .contrib import admin
88from django .core .exceptions import ValidationError
9- from django .utils .html import format_html
9+ from django .utils .html import escape , format_html
1010from django .utils .safestring import mark_safe
1111
1212from openedx_ai_extensions .models import PromptTemplate
@@ -30,7 +30,7 @@ def get_fieldsets(self, request, obj=None):
3030 if obj and obj .pk :
3131 # Editing existing - show UUID example
3232 identification_description = (
33- f'Slug is human-readable, ID is the stable UUID reference. <br/>'
33+ f'Slug is human-readable, ID is the stable UUID reference.<br/>'
3434 f'Use in profile: <code>"prompt_template": "{ obj .pk } "</code> or '
3535 f'<code>"prompt_template": "{ obj .slug } "</code>'
3636 )
@@ -43,15 +43,15 @@ def get_fieldsets(self, request, obj=None):
4343 return (
4444 ('Identification' , {
4545 'fields' : ('slug' , 'id' ),
46- 'description' : format_html (identification_description )
46+ 'description' : format_html (identification_description ),
4747 }),
4848 ('Prompt Content' , {
4949 'fields' : ('body' ,),
50- 'description' : 'The prompt template text - edit in the big textbox below.'
50+ 'description' : 'The prompt template text - edit in the big textbox below.' ,
5151 }),
5252 ('Timestamps' , {
5353 'fields' : ('created_at' , 'updated_at' ),
54- 'classes' : ('collapse' ,)
54+ 'classes' : ('collapse' ,),
5555 }),
5656 )
5757
@@ -63,7 +63,7 @@ def get_form(self, request, obj=None, change=False, **kwargs):
6363 'rows' : 25 ,
6464 'cols' : 120 ,
6565 'class' : 'vLargeTextField' ,
66- 'style' : 'font-family: monospace; font-size: 14px;'
66+ 'style' : 'font-family: monospace; font-size: 14px;' ,
6767 })
6868 return form
6969
@@ -73,6 +73,7 @@ def body_preview(self, obj):
7373 preview = obj .body [:80 ].replace ('\n ' , ' ' )
7474 return preview + ('...' if len (obj .body ) > 80 else '' )
7575 return '-'
76+
7677 body_preview .short_description = 'Prompt Preview'
7778
7879
@@ -89,7 +90,7 @@ class Meta:
8990 'rows' : 20 ,
9091 'cols' : 80 ,
9192 'class' : 'vLargeTextField' ,
92- 'style' : 'font-family: monospace;'
93+ 'style' : 'font-family: monospace;' ,
9394 }),
9495 }
9596
@@ -120,11 +121,9 @@ def clean_content_patch(self):
120121 # Validate JSON5 syntax
121122 try :
122123 parse_json5_string (content_patch_raw )
123- except Exception as e :
124- # json5 library may not expose JSON5DecodeError in all versions
125- raise ValidationError (f'Invalid JSON5 syntax: { e } ' ) from e
124+ except Exception as exc :
125+ raise ValidationError (f'Invalid JSON5 syntax: { exc } ' ) from exc
126126
127- # Return the raw string (we store it as text, parse at runtime)
128127 return content_patch_raw
129128
130129
@@ -142,7 +141,7 @@ class AIWorkflowProfileAdmin(admin.ModelAdmin):
142141
143142 fieldsets = (
144143 ('Basic Information' , {
145- 'fields' : ('slug' , 'description' )
144+ 'fields' : ('slug' , 'description' ),
146145 }),
147146 ('Profile Template Configuration' , {
148147 'fields' : ('base_filepath' , 'base_template_preview' , 'content_patch' ),
@@ -168,12 +167,10 @@ def is_valid(self, obj):
168167 """Show validation status with icon."""
169168 is_valid , errors = obj .validate ()
170169 if is_valid :
171- return format_html (
172- '<span style="color: green;">✓ Valid</span>'
173- )
170+ return format_html ('<span class="ai-admin-preview--success">✓ Valid</span>' )
174171 return format_html (
175- '<span style="color: red; ">✗ {} errors</span>' ,
176- len (errors )
172+ '<span class="ai-admin-preview--error ">✗ {} errors</span>' ,
173+ len (errors ),
177174 )
178175 is_valid .short_description = 'Status'
179176
@@ -189,73 +186,40 @@ def base_template_preview(self, obj):
189186
190187 if not is_safe_template_path (obj .base_filepath ):
191188 return format_html (
192- '<div style="background: #fee; padding: 10px; border-radius: 4px; color: #c00; ">'
189+ '<div class="ai-admin-preview ai-admin-preview--error ">'
193190 '<strong>Error:</strong> Invalid or unsafe template path'
194191 '</div>'
195192 )
196193
197- try :
198- # Find and read the template file as-is
199- template_dirs = get_template_directories ()
200- file_content = None
201-
202- for base_dir in template_dirs :
203- full_path = base_dir / obj .base_filepath
204- if full_path .exists ():
205- with open (full_path , 'r' , encoding = 'utf-8' ) as f :
206- file_content = f .read ()
207- break
208-
209- if file_content is None :
210- return format_html (
211- '<div style="background: #fee; padding: 10px; '
212- 'border-radius: 4px; color: #c00;">'
213- '<strong>Error:</strong> Template file not found'
214- '</div>'
215- )
216-
217- # Generate unique ID for this preview
218- preview_id = f"base-template-{ obj .pk or 'new' } "
194+ file_content = None
195+ for base_dir in get_template_directories ():
196+ full_path = base_dir / obj .base_filepath
197+ if full_path .exists ():
198+ file_content = full_path .read_text (encoding = 'utf-8' )
199+ break
219200
201+ if file_content is None :
220202 return format_html (
221- '<div>'
222- '<a href="#" onclick="'
223- 'var el = document.getElementById(\' {id}\' ); '
224- 'var link = this; '
225- 'if (el.style.display === \' none\' ) {{ '
226- ' el.style.display = \' block\' ; '
227- ' link.textContent = '
228- '\' ▼ Hide Base Template ({filepath})\' ; '
229- '}} else {{ '
230- ' el.style.display = \' none\' ; '
231- ' link.textContent = '
232- '\' ▶ Show Base Template ({filepath})\' ; '
233- '}} '
234- 'return false;" '
235- 'style="text-decoration: none; color: #447e9b; font-weight: bold;">'
236- '▶ Show Base Template ({filepath})'
237- '</a>'
238- '<div id="{id}" style="display: none; '
239- 'background: #f0f8ff; padding: 10px; '
240- 'border-radius: 4px; border: 1px solid #ddd; '
241- 'margin-top: 10px;">'
242- '<pre style="margin: 0; font-family: monospace; '
243- 'font-size: 12px; max-height: 400px; '
244- 'overflow-y: auto;">{content}</pre>'
203+ '<div class="ai-admin-preview ai-admin-preview--error">'
204+ '<strong>Error:</strong> Template file not found'
245205 '</div>'
246- '</div>' ,
247- id = preview_id ,
248- filepath = obj .base_filepath ,
249- content = file_content
250- )
251- except Exception as e : # pylint: disable=broad-exception-caught
252- return format_html (
253- '<div style="background: #fee; padding: 10px; '
254- 'border-radius: 4px; color: #c00;">'
255- '<strong>Error loading template:</strong> {}'
256- '</div>' ,
257- str (e )
258206 )
207+
208+ preview_id = f'base-template-{ obj .pk or "new" } '
209+
210+ return format_html (
211+ '<a href="#" class="ai-admin-toggle" '
212+ 'onclick="var el=document.getElementById(\' {id}\' );'
213+ 'el.style.display = el.style.display === \' none\' ? \' block\' : \' none\' ;'
214+ 'return false;">'
215+ '▶ Toggle Base Template ({path})</a>'
216+ '<div id="{id}" class="ai-admin-preview" style="display:none;">'
217+ '<pre>{content}</pre>'
218+ '</div>' ,
219+ id = preview_id ,
220+ path = obj .base_filepath ,
221+ content = escape (file_content ),
222+ )
259223 base_template_preview .short_description = 'Base Template (Read-Only)'
260224
261225 def effective_config_preview (self , obj ):
@@ -264,23 +228,20 @@ def effective_config_preview(self, obj):
264228 return '-'
265229
266230 try :
267- config = obj .config
268- formatted_json = json .dumps (config , indent = 2 , sort_keys = True )
269-
231+ formatted = json .dumps (obj .config , indent = 2 , sort_keys = True )
270232 return format_html (
271- '<div style="background: #f8f8f8; padding: 10px; border-radius: 4px; ">'
272- '<strong>Effective Configuration (Base Template + Overrides) :</strong><br >'
273- '<pre style="margin-top: 10px; font-family: monospace; font-size: 12px;" >{}</pre>'
233+ '<div class="ai-admin-preview ">'
234+ '<strong>Effective Configuration:</strong>'
235+ '<pre>{}</pre>'
274236 '</div>' ,
275- formatted_json
237+ formatted ,
276238 )
277- except Exception as e : # pylint: disable=broad-exception-caught
239+ except Exception as exc : # pylint: disable=broad-exception-caught
278240 return format_html (
279- '<div style="background: #fee; padding: 10px; '
280- 'border-radius: 4px; color: #c00;">'
281- '<strong>Error loading configuration:</strong> {}'
241+ '<div class="ai-admin-preview ai-admin-preview--error">'
242+ '<strong>Error:</strong> {}'
282243 '</div>' ,
283- str ( e )
244+ exc ,
284245 )
285246 effective_config_preview .short_description = 'Effective Configuration'
286247
@@ -293,25 +254,25 @@ def validation_status(self, obj):
293254
294255 if is_valid :
295256 return format_html (
296- '<div style="background: #efe; padding: 10px; border-radius: 4px; color: #060; ">'
297- '<strong> ✓ Configuration is valid</strong> '
257+ '<div class="ai-admin-preview ai-admin-preview--success ">'
258+ '✓ Configuration is valid'
298259 '</div>'
299260 )
300261
301- error_list = '<br>' .join (f'• { error } ' for error in errors )
262+ error_list = '<br>' .join (f'• { escape ( e ) } ' for e in errors )
302263 return format_html (
303- '<div style="background: #fee; padding: 10px; border-radius: 4px; color: #c00; ">'
304- '<strong>✗ Validation Errors :</strong><br>{}'
264+ '<div class="ai-admin-preview ai-admin-preview--error ">'
265+ '<strong>Validation errors :</strong><br>{}'
305266 '</div>' ,
306- mark_safe (error_list )
267+ mark_safe (error_list ),
307268 )
308269 validation_status .short_description = 'Validation Status'
309270
310271 class Media :
311272 """Admin media assets."""
312273
313274 css = {
314- 'all' : ('admin/css/forms .css' ,)
275+ 'all' : ('openedx_ai_extensions/admin .css' ,),
315276 }
316277
317278
@@ -321,11 +282,7 @@ class AIWorkflowSessionAdmin(admin.ModelAdmin):
321282 Admin interface for managing AI Workflow Sessions.
322283 """
323284
324- list_display = (
325- "user" ,
326- "course_id" ,
327- "location_id" ,
328- )
285+ list_display = ("user" , "course_id" , "location_id" )
329286 search_fields = ("user__username" , "course_id" , "location_id" )
330287 readonly_fields = ("local_submission_id" , "remote_response_id" , "metadata" )
331288
0 commit comments