220 lines
7.7 KiB
Python
220 lines
7.7 KiB
Python
import os
|
|
from django.db import models
|
|
from django.conf import settings
|
|
|
|
|
|
CATEGORY_CHOICES = [
|
|
('frontend', '前端'),
|
|
('backend', '后端'),
|
|
('tools', '工具'),
|
|
('devops', '运维'),
|
|
('security', '安全'),
|
|
('basic', '基础'),
|
|
]
|
|
|
|
LEVEL_CHOICES = [
|
|
('beginner', '入门'),
|
|
('advanced', '进阶'),
|
|
]
|
|
|
|
STATUS_CHOICES = [
|
|
('published', '已发布'),
|
|
('draft', '草稿'),
|
|
]
|
|
|
|
|
|
class Course(models.Model):
|
|
title = models.CharField(max_length=200, verbose_name='标题')
|
|
description = models.TextField(blank=True, default='', verbose_name='描述')
|
|
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES, default='frontend')
|
|
level = models.CharField(max_length=10, choices=LEVEL_CHOICES, default='beginner')
|
|
cover_image = models.ImageField(upload_to='learn/covers/%Y/%m/', blank=True, null=True)
|
|
color = models.CharField(max_length=20, default='#667eea', verbose_name='主题色')
|
|
icon_name = models.CharField(max_length=50, default='CodeOutlined', verbose_name='图标名称')
|
|
author = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='courses',
|
|
verbose_name='作者'
|
|
)
|
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
|
|
sort_order = models.IntegerField(default=0)
|
|
is_hot = models.BooleanField(default=False)
|
|
is_new = models.BooleanField(default=False)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ['-sort_order', '-created_at']
|
|
verbose_name = '课程'
|
|
verbose_name_plural = '课程'
|
|
|
|
def __str__(self):
|
|
return self.title
|
|
|
|
|
|
class Chapter(models.Model):
|
|
VIDEO_SOURCE_CHOICES = [
|
|
('bilibili', 'B站视频'),
|
|
('local', '本地视频'),
|
|
]
|
|
|
|
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='chapters', verbose_name='课程')
|
|
title = models.CharField(max_length=200)
|
|
sort_order = models.IntegerField(default=0)
|
|
duration = models.CharField(max_length=50, blank=True, default='', verbose_name='时长')
|
|
is_free = models.BooleanField(default=True)
|
|
video_url = models.URLField(max_length=500, blank=True, default='', verbose_name='视频链接')
|
|
video_source = models.CharField(max_length=20, choices=VIDEO_SOURCE_CHOICES, default='bilibili', verbose_name='视频来源')
|
|
video_local = models.FileField(upload_to='learn/videos/%Y/%m/', blank=True, null=True, verbose_name='本地视频文件')
|
|
video_poster = models.ImageField(upload_to='learn/video_posters/%Y/%m/', blank=True, null=True, verbose_name='视频封面')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ['sort_order']
|
|
verbose_name = '章节'
|
|
verbose_name_plural = '章节'
|
|
|
|
def __str__(self):
|
|
return self.title
|
|
|
|
|
|
class ChapterContent(models.Model):
|
|
chapter = models.OneToOneField(Chapter, on_delete=models.CASCADE, related_name='content', verbose_name='章节')
|
|
content_md = models.TextField(blank=True, default='', verbose_name='Markdown内容')
|
|
content_html = models.TextField(blank=True, default='', verbose_name='预渲染HTML')
|
|
md_file_path = models.CharField(max_length=500, blank=True, default='', verbose_name='MD文件路径')
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
verbose_name = '章节内容'
|
|
verbose_name_plural = '章节内容'
|
|
|
|
def __str__(self):
|
|
return f'{self.chapter.title} - 内容'
|
|
|
|
def save(self, *args, **kwargs):
|
|
super().save(*args, **kwargs)
|
|
if self.content_md:
|
|
try:
|
|
import markdown
|
|
self.content_html = markdown.markdown(
|
|
self.content_md,
|
|
extensions=['extra', 'codehilite', 'toc', 'tables']
|
|
)
|
|
except ImportError:
|
|
self.content_html = self.content_md.replace('\n', '<br>')
|
|
ChapterContent.objects.filter(pk=self.pk).update(content_html=self.content_html)
|
|
self.export_md_file()
|
|
|
|
def export_md_file(self):
|
|
if not self.content_md:
|
|
return
|
|
course_id = self.chapter.course_id
|
|
chapter_id = self.chapter_id
|
|
file_path = os.path.join(
|
|
settings.MEDIA_ROOT,
|
|
'learn', 'courses', str(course_id), 'chapters', f'{chapter_id}.md'
|
|
)
|
|
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
|
with open(file_path, 'w', encoding='utf-8') as f:
|
|
f.write(self.content_md)
|
|
relative_path = os.path.relpath(file_path, settings.MEDIA_ROOT).replace('\\', '/')
|
|
ChapterContent.objects.filter(pk=self.pk).update(md_file_path=relative_path)
|
|
|
|
|
|
class ChapterRead(models.Model):
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='chapter_reads',
|
|
verbose_name='用户'
|
|
)
|
|
chapter = models.ForeignKey(
|
|
Chapter,
|
|
on_delete=models.CASCADE,
|
|
related_name='chapter_reads',
|
|
verbose_name='章节'
|
|
)
|
|
course = models.ForeignKey(
|
|
Course,
|
|
on_delete=models.CASCADE,
|
|
related_name='chapter_reads',
|
|
verbose_name='课程'
|
|
)
|
|
completed = models.BooleanField(default=True, verbose_name='是否已完成')
|
|
completed_at = models.DateTimeField(auto_now_add=True, verbose_name='完成时间')
|
|
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
|
|
|
class Meta:
|
|
verbose_name = '章节学习记录'
|
|
verbose_name_plural = '章节学习记录'
|
|
unique_together = ['user', 'chapter']
|
|
ordering = ['-completed_at']
|
|
|
|
def __str__(self):
|
|
return f'{self.user} read {self.chapter}'
|
|
|
|
|
|
class CourseFavorite(models.Model):
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='course_favorites',
|
|
verbose_name='用户'
|
|
)
|
|
course = models.ForeignKey(
|
|
Course,
|
|
on_delete=models.CASCADE,
|
|
related_name='course_favorites',
|
|
verbose_name='课程'
|
|
)
|
|
created_at = models.DateTimeField(
|
|
auto_now_add=True,
|
|
verbose_name='创建时间'
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = '课程收藏'
|
|
verbose_name_plural = '课程收藏'
|
|
unique_together = ['user', 'course']
|
|
ordering = ['-created_at']
|
|
|
|
def __str__(self):
|
|
return f'{self.user} favorites {self.course}'
|
|
|
|
|
|
class CourseMaterial(models.Model):
|
|
FILE_TYPE_CHOICES = [
|
|
('pdf', 'PDF'),
|
|
('code', '代码'),
|
|
('doc', '文档'),
|
|
('video', '视频'),
|
|
('dataset', '数据集'),
|
|
('zip', '压缩包'),
|
|
('other', '其他'),
|
|
]
|
|
|
|
chapter = models.ForeignKey(
|
|
Chapter,
|
|
on_delete=models.CASCADE,
|
|
related_name='materials',
|
|
verbose_name='章节'
|
|
)
|
|
title = models.CharField(max_length=200, verbose_name='资料名称')
|
|
file = models.FileField(upload_to='learn/materials/%Y/%m/', verbose_name='文件')
|
|
file_type = models.CharField(max_length=20, choices=FILE_TYPE_CHOICES, default='other', verbose_name='文件类型')
|
|
file_size = models.PositiveIntegerField(default=0, verbose_name='文件大小(字节)')
|
|
download_count = models.PositiveIntegerField(default=0, verbose_name='下载次数')
|
|
sort_order = models.IntegerField(default=0, verbose_name='排序')
|
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
|
|
|
class Meta:
|
|
ordering = ['sort_order']
|
|
verbose_name = '课程资料'
|
|
verbose_name_plural = '课程资料'
|
|
|
|
def __str__(self):
|
|
return self.title
|