diff --git a/forum/form.py b/forum/form.py index d73e9a0..05ff949 100644 --- a/forum/form.py +++ b/forum/form.py @@ -1,4 +1,4 @@ -from .models import Post, Comment +from .models import Post, Comment, Collection from django import forms import re @@ -30,6 +30,32 @@ def save(self, commit=True): instance.save() return instance +class CollectionForm(forms.ModelForm): + def __init__(self, *args, user=None, **kwargs): + super().__init__(*args, **kwargs) + self.user = user + + class Meta: + model = Collection + fields = ['name', 'description'] + labels = { + 'name': '合集名称', + 'description': '描述', + } + widgets = { + 'name': forms.TextInput(attrs={'class': 'form-control'}), + 'description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}), + } + + def save(self, commit=True): + instance = super().save(commit=False) + if self.user and not instance.pk: + instance.owner = self.user + if commit: + instance.save() + return instance + + # TODO: support @xxx class MDEditorCommentForm(forms.ModelForm): def __init__(self, *args, user=None, post=None, **kwargs): diff --git a/forum/migrations/0005_collection_collectionpost.py b/forum/migrations/0005_collection_collectionpost.py new file mode 100644 index 0000000..4be0277 --- /dev/null +++ b/forum/migrations/0005_collection_collectionpost.py @@ -0,0 +1,42 @@ +# Generated by Django 6.0.2 on 2026-02-24 07:24 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('forum', '0004_post_views'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Collection', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('description', models.TextField(blank=True, default='')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collections', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='CollectionPost', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveIntegerField(default=0)), + ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collection_posts', to='forum.collection')), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collection_entries', to='forum.post')), + ], + options={ + 'ordering': ['order'], + 'unique_together': {('collection', 'post')}, + }, + ), + ] diff --git a/forum/migrations/0006_collection_description_html.py b/forum/migrations/0006_collection_description_html.py new file mode 100644 index 0000000..5203a08 --- /dev/null +++ b/forum/migrations/0006_collection_description_html.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.2 on 2026-02-24 07:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('forum', '0005_collection_collectionpost'), + ] + + operations = [ + migrations.AddField( + model_name='collection', + name='description_html', + field=models.TextField(blank=True, editable=False), + ), + ] diff --git a/forum/models.py b/forum/models.py index 53e7cfb..0880d48 100644 --- a/forum/models.py +++ b/forum/models.py @@ -3,6 +3,9 @@ from .markdown import MarkdownModel +import markdown +import bleach + # Create your models here. class Item(MarkdownModel): @@ -42,3 +45,43 @@ class Comment(MarkdownModel): def __str__(self): return f"{self.author} comment {self.post}" + + +class Collection(models.Model): + owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='collections') + name = models.CharField(max_length=100) + description = models.TextField(blank=True, default='') + description_html = models.TextField(editable=False, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-created_at'] + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + if self.description: + html = markdown.markdown(self.description, extensions=MarkdownModel.MARKDOWN_EXTENSIONS) + self.description_html = bleach.clean( + html, + tags=MarkdownModel.allowed_tags, + attributes=MarkdownModel.allowed_attrs, + protocols=MarkdownModel.ALLOWED_PROTOCOLS, + ) + else: + self.description_html = '' + super().save(*args, **kwargs) + + +class CollectionPost(models.Model): + collection = models.ForeignKey(Collection, on_delete=models.CASCADE, related_name='collection_posts') + post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='collection_entries') + order = models.PositiveIntegerField(default=0) + + class Meta: + ordering = ['order'] + unique_together = ('collection', 'post') + + def __str__(self): + return f"{self.collection.name} - {self.post.title}" diff --git a/forum/templates/base.html b/forum/templates/base.html index dbce856..79f9c83 100644 --- a/forum/templates/base.html +++ b/forum/templates/base.html @@ -68,6 +68,9 @@ + diff --git a/forum/templates/forum/collection_check_delete.html b/forum/templates/forum/collection_check_delete.html new file mode 100644 index 0000000..b3b8d37 --- /dev/null +++ b/forum/templates/forum/collection_check_delete.html @@ -0,0 +1,36 @@ +{% extends 'base.html' %} + +{% block title %}确认删除合集{% endblock %} + +{% block content %} +
+
+
+
+
+
删除合集 "{{ object.name }}"
+
+ {% csrf_token %} +

合集将被永久删除,但其中的文章不会被删除。

+

请输入合集名称 {{ object.name }} 以确认:

+ +
+
+ + 取消 +
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/forum/templates/forum/collection_detail.html b/forum/templates/forum/collection_detail.html new file mode 100644 index 0000000..465b31f --- /dev/null +++ b/forum/templates/forum/collection_detail.html @@ -0,0 +1,36 @@ +{% extends 'base.html' %} + +{% block title %}{{ collection.name }}{% endblock %} + +{% block content %} +

{{ collection.name }}

+{% if collection.description_html %} +
{{ collection.description_html|safe }}
+{% endif %} + +

+ 创建者:{{ collection.owner.username }} · {{ collection.created_at|date:"Y-m-d" }} +

+ +{% if user == collection.owner %} +
+ 编辑 + 管理文章 + 删除 +
+{% endif %} + +
    +{% for cp in collection_posts %} +
  1. +
    + {{ cp.post.title }} + {{ cp.post.author.username }} +
    + {{ cp.post.created_at|date:"Y-m-d" }} +
  2. +{% empty %} +
  3. 合集中暂无文章
  4. +{% endfor %} +
+{% endblock %} diff --git a/forum/templates/forum/collection_form.html b/forum/templates/forum/collection_form.html new file mode 100644 index 0000000..7a483bb --- /dev/null +++ b/forum/templates/forum/collection_form.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +

{{ title }}

+ +
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.errors %} +
{{ field.errors }}
+ {% endif %} +
+ {% endfor %} + + 取消 +
+{% endblock %} diff --git a/forum/templates/forum/collection_list.html b/forum/templates/forum/collection_list.html new file mode 100644 index 0000000..9e9c854 --- /dev/null +++ b/forum/templates/forum/collection_list.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block title %}合集{% endblock %} + +{% block content %} +

合集

+{% if user.is_authenticated %} +创建合集 +{% endif %} + + +{% endblock %} diff --git a/forum/templates/forum/collection_manage.html b/forum/templates/forum/collection_manage.html new file mode 100644 index 0000000..0759949 --- /dev/null +++ b/forum/templates/forum/collection_manage.html @@ -0,0 +1,75 @@ +{% extends 'base.html' %} + +{% block title %}管理合集 - {{ collection.name }}{% endblock %} + +{% block content %} +

管理合集:{{ collection.name }}

+ +

当前文章

+{% if collection_posts %} + +

拖拽条目可调整顺序,松手自动保存。

+{% else %} +

合集中暂无文章

+{% endif %} + +

添加文章

+{% if available_posts %} +
+ {% csrf_token %} + +
+ {% for post in available_posts %} + + {% endfor %} +
+ +
+{% else %} +

没有可添加的文章

+{% endif %} + +返回合集 + + + +{% endblock %} diff --git a/forum/templates/forum/post_add_to_collection.html b/forum/templates/forum/post_add_to_collection.html new file mode 100644 index 0000000..b166142 --- /dev/null +++ b/forum/templates/forum/post_add_to_collection.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block title %}添加到合集{% endblock %} + +{% block content %} +

将「{{ post.title }}」添加到合集

+ +{% if collections %} +
+ {% csrf_token %} +
+ {% for collection in collections %} + + {% endfor %} +
+ + 取消 +
+{% else %} +

你还没有创建任何合集。

+创建合集 +返回 +{% endif %} +{% endblock %} diff --git a/forum/templates/forum/post_detail.html b/forum/templates/forum/post_detail.html index 867388e..38baea5 100644 --- a/forum/templates/forum/post_detail.html +++ b/forum/templates/forum/post_detail.html @@ -5,6 +5,28 @@ {% block content %}
+ {% if collection %} + + {% endif %} +

{{ post.title }}

@@ -21,6 +43,9 @@

{{ post.title }}

+ + + @@ -111,6 +136,30 @@

发表评论

{% endif %} +{% if collection %} +
+
+
+ {{ collection.name }} +
+ +
+
+
    + {% for cp in collection_posts_all %} +
  1. + {% if cp.post.id == post.id %} + {{ cp.post.title }} + {% else %} + {{ cp.post.title }} + {% endif %} +
  2. + {% endfor %} +
+
+
+{% endif %} + {% endblock %} diff --git a/forum/templates/forum/post_list.html b/forum/templates/forum/post_list.html index e3b624c..8b0d24b 100644 --- a/forum/templates/forum/post_list.html +++ b/forum/templates/forum/post_list.html @@ -6,11 +6,20 @@

所有文章

发新帖