# 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), ]