feat(platform): ship AI fallback stack and entity-scoped course planning
Unifies the new LangGraph-driven course-plan/media flow with robust provider fallbacks, admin AI provider settings, editable book-style materials, and strict entity isolation across LMS/course-plan APIs. Adds admin-only entity membership management in the Entities UI so users can switch linked entities directly from the platform. Made-with: Cursor
This commit is contained in:
@@ -12,6 +12,13 @@ class EncoachResource(models.Model):
|
||||
('document', 'Document'),
|
||||
('link', 'Link'),
|
||||
('interactive', 'Interactive'),
|
||||
# Audio + image are surfaced in the Resource Manager table
|
||||
# (icons / filters) so we accept them as first-class types
|
||||
# rather than coercing them into ``document`` and losing the
|
||||
# MIME-correct preview (audio player, <img>, etc.).
|
||||
('audio', 'Audio'),
|
||||
('image', 'Image'),
|
||||
('article', 'Article'),
|
||||
])
|
||||
review_status = fields.Selection([
|
||||
('pending', 'Pending'),
|
||||
@@ -30,6 +37,24 @@ class EncoachResource(models.Model):
|
||||
'resource_id', 'tag_id', string='Tags',
|
||||
)
|
||||
file = fields.Binary(attachment=True)
|
||||
# Preserve the original upload filename (with extension) so the
|
||||
# download endpoint can serve "report.pdf" even when the human-
|
||||
# readable ``name`` field is set to "Q3 Sales Report" without an
|
||||
# extension. We also keep the resolved MIME type to avoid sniffing
|
||||
# by extension on every download — sniffing fails on resources
|
||||
# named "test" with no dot, and was the root cause of admins
|
||||
# downloading files with no extension and ``application/octet-stream``
|
||||
# ending up unviewable on macOS / Windows.
|
||||
original_filename = fields.Char(
|
||||
string='Original filename',
|
||||
help='Filename as uploaded, including extension. Used by the '
|
||||
'download endpoint to produce a sensible Content-Disposition.',
|
||||
)
|
||||
mimetype = fields.Char(
|
||||
string='MIME type',
|
||||
help='Cached MIME type from the uploaded file or URL. Drives '
|
||||
'preview decisions (PDF in iframe vs <img> vs <video>).',
|
||||
)
|
||||
url = fields.Char()
|
||||
difficulty = fields.Selection([
|
||||
('beginner', 'Beginner'), ('intermediate', 'Intermediate'), ('advanced', 'Advanced'),
|
||||
@@ -67,6 +92,16 @@ class EncoachResource(models.Model):
|
||||
def to_api_dict(self):
|
||||
self.ensure_one()
|
||||
creator = self.creator_id
|
||||
# Both URLs are JWT-protected via the resources controller; the
|
||||
# frontend appends ``?token=…`` with the existing
|
||||
# ``withAuthQuery`` helper so they can be used as ``href``,
|
||||
# ``src`` of <iframe>/<img>, or download anchors.
|
||||
download_url = (
|
||||
f'/api/resources/{self.id}/download' if self.file else ''
|
||||
)
|
||||
preview_url = (
|
||||
f'/api/resources/{self.id}/download?inline=1' if self.file else ''
|
||||
)
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
@@ -82,6 +117,10 @@ class EncoachResource(models.Model):
|
||||
'learning_objective_names': self.learning_objective_ids.mapped('name'),
|
||||
'url': self.url or '',
|
||||
'has_file': bool(self.file),
|
||||
'mimetype': self.mimetype or '',
|
||||
'original_filename': self.original_filename or '',
|
||||
'download_url': download_url,
|
||||
'preview_url': preview_url,
|
||||
'difficulty': self.difficulty or '',
|
||||
'duration_minutes': self.duration_minutes,
|
||||
'author_id': creator.id if creator else None,
|
||||
@@ -98,5 +137,5 @@ class EncoachResource(models.Model):
|
||||
'approved': self.approved,
|
||||
'course_count': self.course_count,
|
||||
'created_at': self.create_date.isoformat() if self.create_date else '',
|
||||
'file_name': self.name,
|
||||
'file_name': self.original_filename or self.name,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user