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..e0b85dc 100644
--- a/forum/templates/base.html
+++ b/forum/templates/base.html
@@ -43,7 +43,7 @@
{% block extra_head %}{% endblock %}
-
+