sync from local backup
This commit is contained in:
+219
@@ -0,0 +1,219 @@
|
||||
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
|
||||
Reference in New Issue
Block a user