feat(backend): add blog publishing platform
This commit is contained in:
297
apps/blog/migrations/0003_blog_platform.py
Normal file
297
apps/blog/migrations/0003_blog_platform.py
Normal file
@@ -0,0 +1,297 @@
|
||||
# Generated by Django 5.2.5 on 2026-06-08 17:29
|
||||
|
||||
import apps.blog.models
|
||||
import django.db.models.deletion
|
||||
import markdown
|
||||
import re
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def backfill_post_render_fields(apps, schema_editor):
|
||||
Post = apps.get_model('blog', 'Post')
|
||||
for post in Post.objects.all():
|
||||
html = markdown.markdown(
|
||||
post.content or '',
|
||||
extensions=[
|
||||
'markdown.extensions.extra',
|
||||
'markdown.extensions.codehilite',
|
||||
'markdown.extensions.toc',
|
||||
],
|
||||
)
|
||||
plain_text = re.sub(r'<[^<]+?>', ' ', html).replace('\n', ' ').strip()
|
||||
post.content_html = html
|
||||
word_count = len((post.content or '').split())
|
||||
post.reading_time = max(1, (word_count + 199) // 200)
|
||||
if not post.excerpt and plain_text:
|
||||
post.excerpt = f'{plain_text[:297]}...' if len(plain_text) > 300 else plain_text
|
||||
post.save(update_fields=['content_html', 'reading_time', 'excerpt'])
|
||||
|
||||
|
||||
def seed_blog_role_groups(apps, schema_editor):
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
Group = apps.get_model('auth', 'Group')
|
||||
|
||||
post_ct, _ = ContentType.objects.get_or_create(app_label='blog', model='post')
|
||||
category_ct, _ = ContentType.objects.get_or_create(app_label='blog', model='category')
|
||||
tag_ct, _ = ContentType.objects.get_or_create(app_label='blog', model='tag')
|
||||
|
||||
permission_specs = [
|
||||
(post_ct, 'access_blog_admin', 'Can access blog admin'),
|
||||
(post_ct, 'review_blog_post', 'Can review blog posts'),
|
||||
(post_ct, 'publish_blog_post', 'Can publish blog posts'),
|
||||
(post_ct, 'moderate_blog_comment', 'Can moderate blog comments'),
|
||||
(post_ct, 'upload_blog_asset', 'Can upload blog assets'),
|
||||
(post_ct, 'add_post', 'Can add post'),
|
||||
(post_ct, 'change_post', 'Can change post'),
|
||||
(category_ct, 'add_category', 'Can add category'),
|
||||
(category_ct, 'change_category', 'Can change category'),
|
||||
(tag_ct, 'add_tag', 'Can add tag'),
|
||||
(tag_ct, 'change_tag', 'Can change tag'),
|
||||
]
|
||||
permissions = {}
|
||||
for content_type, codename, name in permission_specs:
|
||||
permission, _ = Permission.objects.get_or_create(
|
||||
content_type=content_type,
|
||||
codename=codename,
|
||||
defaults={'name': name},
|
||||
)
|
||||
permissions[codename] = permission
|
||||
|
||||
editor, _ = Group.objects.get_or_create(name='blog_editor')
|
||||
editor.permissions.add(
|
||||
permissions['add_post'],
|
||||
permissions['change_post'],
|
||||
permissions['access_blog_admin'],
|
||||
permissions['upload_blog_asset'],
|
||||
)
|
||||
|
||||
supervisor, _ = Group.objects.get_or_create(name='blog_supervisor')
|
||||
supervisor.permissions.add(
|
||||
permissions['add_post'],
|
||||
permissions['change_post'],
|
||||
permissions['access_blog_admin'],
|
||||
permissions['upload_blog_asset'],
|
||||
permissions['review_blog_post'],
|
||||
permissions['publish_blog_post'],
|
||||
permissions['moderate_blog_comment'],
|
||||
permissions['add_category'],
|
||||
permissions['change_category'],
|
||||
permissions['add_tag'],
|
||||
permissions['change_tag'],
|
||||
)
|
||||
|
||||
Group.objects.get_or_create(name='association_admin')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
('blog', '0002_initial'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PostAsset',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('is_deleted', models.BooleanField(default=False)),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||
('file', models.FileField(upload_to=apps.blog.models.post_asset_upload_to)),
|
||||
('file_type', models.CharField(choices=[('image', 'Image'), ('video', 'Video'), ('document', 'Document'), ('archive', 'Archive'), ('other', 'Other')], default='other', max_length=16)),
|
||||
('title', models.CharField(blank=True, max_length=200)),
|
||||
('alt_text', models.CharField(blank=True, max_length=200)),
|
||||
('caption', models.TextField(blank=True)),
|
||||
('size', models.PositiveBigIntegerField(default=0)),
|
||||
('mime_type', models.CharField(blank=True, max_length=120)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SavedPost',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='post',
|
||||
options={'ordering': ['-created_at'], 'permissions': [('access_blog_admin', 'Can access blog admin'), ('review_blog_post', 'Can review blog posts'), ('publish_blog_post', 'Can publish blog posts'), ('moderate_blog_comment', 'Can moderate blog comments'), ('upload_blog_asset', 'Can upload blog assets')]},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='comment',
|
||||
name='hidden_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='comment',
|
||||
name='hidden_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hidden_blog_comments', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='comment',
|
||||
name='moderation_note',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='canonical_url',
|
||||
field=models.URLField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='content_html',
|
||||
field=models.TextField(blank=True, editable=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='focus_keyword',
|
||||
field=models.CharField(blank=True, max_length=120),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='noindex',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='og_description',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='og_image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='blog/og/'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='og_title',
|
||||
field=models.CharField(blank=True, max_length=95),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='published_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='published_blog_posts', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='reading_time',
|
||||
field=models.PositiveIntegerField(default=1),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='review_note',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='reviewed_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='reviewed_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_blog_posts', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='seo_description',
|
||||
field=models.CharField(blank=True, max_length=170),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='seo_title',
|
||||
field=models.CharField(blank=True, max_length=70),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='post',
|
||||
name='submitted_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='slug',
|
||||
field=models.SlugField(allow_unicode=True, blank=True, max_length=100, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='post',
|
||||
name='slug',
|
||||
field=models.SlugField(allow_unicode=True, blank=True, max_length=200, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='post',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('draft', 'Draft'), ('submitted', 'Submitted for review'), ('changes_requested', 'Changes requested'), ('published', 'Published'), ('archived', 'Archived')], default='draft', max_length=24),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tag',
|
||||
name='slug',
|
||||
field=models.SlugField(allow_unicode=True, blank=True, unique=True),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='comment',
|
||||
index=models.Index(fields=['author', 'created_at'], name='blog_commen_author__9faedb_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='like',
|
||||
index=models.Index(fields=['user', 'created_at'], name='blog_like_user_id_7a46aa_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='post',
|
||||
index=models.Index(fields=['author', 'status'], name='blog_post_author__95cbf7_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='post',
|
||||
index=models.Index(fields=['slug', 'status'], name='blog_post_slug_714acb_idx'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='postasset',
|
||||
name='post',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='blog.post'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='postasset',
|
||||
name='uploaded_by',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blog_assets', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='savedpost',
|
||||
name='post',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='saves', to='blog.post'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='savedpost',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='saved_posts', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='postasset',
|
||||
index=models.Index(fields=['post', 'file_type'], name='blog_postas_post_id_d81393_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='postasset',
|
||||
index=models.Index(fields=['uploaded_by', 'created_at'], name='blog_postas_uploade_c579a7_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='savedpost',
|
||||
index=models.Index(fields=['post'], name='blog_savedp_post_id_62b622_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='savedpost',
|
||||
index=models.Index(fields=['user', 'created_at'], name='blog_savedp_user_id_c04172_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='savedpost',
|
||||
unique_together={('post', 'user')},
|
||||
),
|
||||
migrations.RunPython(backfill_post_render_fields, migrations.RunPython.noop),
|
||||
migrations.RunPython(seed_blog_role_groups, migrations.RunPython.noop),
|
||||
]
|
||||
Reference in New Issue
Block a user