Browse Source
feat(article): implement structured content model for articles with bilingual text sections
feat(article): implement structured content model for articles with bilingual text sections
- Added new models for `ContentPart` and `TextSection` to support structured article content, allowing for bilingual Arabic text and translations. - Updated serializers to include nested text sections within content parts, enhancing the API response structure. - Introduced a management command to seed article data with Russian content about Imams and Prophets, including categories, collections, and structured content sections. - Enhanced the admin interface to manage text sections within content parts effectively. - Created a sample API response for articles to demonstrate the new structure.master
9 changed files with 774 additions and 21 deletions
-
191apps/api/views/documentation.py
-
48apps/article/admin.py
-
0apps/article/management/__init__.py
-
0apps/article/management/commands/__init__.py
-
445apps/article/management/commands/seed_article_data.py
-
47apps/article/migrations/0003_alter_middlearticlecollection_options_and_more.py
-
30apps/article/models.py
-
9apps/article/serializers.py
-
1apps/hadis/models/category.py
@ -0,0 +1,445 @@ |
|||||
|
from django.core.management.base import BaseCommand |
||||
|
from django.utils.text import slugify |
||||
|
from apps.article.models import ( |
||||
|
ArticleCategory, |
||||
|
ArticleCollection, |
||||
|
Article, |
||||
|
ArticleContent, |
||||
|
ContentPart, |
||||
|
TextSection |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class Command(BaseCommand): |
||||
|
help = 'Seed article data with Russian content about Imams and Prophets' |
||||
|
|
||||
|
def handle(self, *args, **kwargs): |
||||
|
self.stdout.write(self.style.SUCCESS('Starting to seed article data...')) |
||||
|
|
||||
|
# Create categories |
||||
|
self.stdout.write('Creating categories...') |
||||
|
categories = self.create_categories() |
||||
|
|
||||
|
# Create collections |
||||
|
self.stdout.write('Creating collections...') |
||||
|
collections = self.create_collections() |
||||
|
|
||||
|
# Create articles |
||||
|
self.stdout.write('Creating articles...') |
||||
|
articles = self.create_articles(categories, collections) |
||||
|
|
||||
|
self.stdout.write(self.style.SUCCESS(f'Successfully created {len(articles)} articles with content!')) |
||||
|
|
||||
|
def create_categories(self): |
||||
|
categories_data = [ |
||||
|
{'title': 'Пророки', 'order': 1}, |
||||
|
{'title': 'Имамы', 'order': 2}, |
||||
|
{'title': 'Жизнь имамов', 'order': 3}, |
||||
|
{'title': 'Исламская история', 'order': 4}, |
||||
|
] |
||||
|
|
||||
|
categories = [] |
||||
|
for cat_data in categories_data: |
||||
|
category, created = ArticleCategory.objects.get_or_create( |
||||
|
title=cat_data['title'], |
||||
|
defaults={ |
||||
|
'slug': slugify(cat_data['title'], allow_unicode=True), |
||||
|
'order': cat_data['order'], |
||||
|
'status': True |
||||
|
} |
||||
|
) |
||||
|
categories.append(category) |
||||
|
if created: |
||||
|
self.stdout.write(f' Created category: {category.title}') |
||||
|
|
||||
|
return categories |
||||
|
|
||||
|
def create_collections(self): |
||||
|
collections_data = [ |
||||
|
{ |
||||
|
'title': 'Избранные статьи об имамах', |
||||
|
'summary': 'Лучшие статьи о жизни и учениях имамов', |
||||
|
'pin_top': True, |
||||
|
'display_position': 'pinned', |
||||
|
'order': 1 |
||||
|
}, |
||||
|
{ |
||||
|
'title': 'Коллекция о пророках', |
||||
|
'summary': 'Статьи о пророках в исламе', |
||||
|
'pin_top': False, |
||||
|
'display_position': 'middle', |
||||
|
'order': 2 |
||||
|
}, |
||||
|
] |
||||
|
|
||||
|
collections = [] |
||||
|
for coll_data in collections_data: |
||||
|
collection, created = ArticleCollection.objects.get_or_create( |
||||
|
title=coll_data['title'], |
||||
|
defaults={ |
||||
|
'slug': slugify(coll_data['title'], allow_unicode=True), |
||||
|
'summary': coll_data['summary'], |
||||
|
'pin_top': coll_data['pin_top'], |
||||
|
'display_position': coll_data['display_position'], |
||||
|
'order': coll_data['order'], |
||||
|
'status': True |
||||
|
} |
||||
|
) |
||||
|
collections.append(collection) |
||||
|
if created: |
||||
|
self.stdout.write(f' Created collection: {collection.title}') |
||||
|
|
||||
|
return collections |
||||
|
|
||||
|
def create_articles(self, categories, collections): |
||||
|
articles_data = [ |
||||
|
{ |
||||
|
'title': 'Имам Джавад (мир ему)', |
||||
|
'description': 'Биография девятого имама шиитов, Мухаммада ибн Али аль-Джавада', |
||||
|
'categories': [categories[1]], # Имамы |
||||
|
'collections': [collections[0]], |
||||
|
'content_sections': [ |
||||
|
{ |
||||
|
'title': 'Введение', |
||||
|
'content': 'Имам Мухаммад ибн Али аль-Джавад - девятый имам двенадцати имамов в шиитском исламе.\nОн родился в 195 году хиджры в Медине и стал имамом в очень молодом возрасте.\nЕго жизнь полна уроков мудрости и знания.', |
||||
|
'priority': 1, |
||||
|
'parts': [ |
||||
|
{ |
||||
|
'order': 1, |
||||
|
'text_sections': [ |
||||
|
{ |
||||
|
'arabic_text': 'بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ', |
||||
|
'translation': 'Во имя Аллаха, Милостивого, Милосердного', |
||||
|
'order': 1 |
||||
|
}, |
||||
|
{ |
||||
|
'arabic_text': 'الْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ وَالصَّلَاةُ وَالسَّلَامُ عَلَىٰ سَيِّدِنَا مُحَمَّدٍ وَآلِهِ الطَّاهِرِينَ', |
||||
|
'translation': 'Хвала Аллаху, Господу миров, и мир и благословение нашему господину Мухаммаду и его пречистому семейству', |
||||
|
'order': 2 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
'title': 'Рождение и детство', |
||||
|
'content': 'Имам Джавад родился в 195 году хиджры (811 г. н.э.) в священном городе Медина.\nЕго отцом был имам Али ибн Муса ар-Рида, восьмой имам.\nОн стал имамом в возрасте семи или девяти лет после мученической смерти своего отца.', |
||||
|
'priority': 2, |
||||
|
'parts': [ |
||||
|
{ |
||||
|
'order': 1, |
||||
|
'text_sections': [ |
||||
|
{ |
||||
|
'arabic_text': 'وُلِدَ الإِمَامُ مُحَمَّدُ بْنُ عَلِيٍّ الْجَوَادُ فِي الْمَدِينَةِ الْمُنَوَّرَةِ', |
||||
|
'translation': 'Имам Мухаммад ибн Али аль-Джавад родился в благословенной Медине', |
||||
|
'order': 1 |
||||
|
}, |
||||
|
{ |
||||
|
'arabic_text': 'فِي سَنَةِ خَمْسٍ وَتِسْعِينَ وَمِائَةٍ مِنَ الْهِجْرَةِ', |
||||
|
'translation': 'В сто девяносто пятом году хиджры', |
||||
|
'order': 2 |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
'order': 2, |
||||
|
'text_sections': [ |
||||
|
{ |
||||
|
'arabic_text': 'وَكَانَ عُمْرُهُ عِنْدَ الإِمَامَةِ سَبْعَ سِنِينَ أَوْ تِسْعَ سِنِينَ', |
||||
|
'translation': 'Ему было семь или девять лет, когда он стал имамом', |
||||
|
'order': 1 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
'title': 'Знания и мудрость', |
||||
|
'content': 'Несмотря на свой молодой возраст, имам Джавад проявлял невероятные знания и мудрость.\nОн отвечал на сложные вопросы ученых и удивлял их своей проницательностью.\nЕго называли "аль-Джавад" (щедрый) за его великодушие и знания.', |
||||
|
'priority': 3, |
||||
|
'parts': [ |
||||
|
{ |
||||
|
'order': 1, |
||||
|
'text_sections': [ |
||||
|
{ |
||||
|
'arabic_text': 'كَانَ الإِمَامُ الْجَوَادُ آيَةً فِي الْعِلْمِ وَالْحِكْمَةِ', |
||||
|
'translation': 'Имам Джавад был знамением в знании и мудрости', |
||||
|
'order': 1 |
||||
|
}, |
||||
|
{ |
||||
|
'arabic_text': 'وَأَجَابَ عَلَى أَسْئِلَةِ الْعُلَمَاءِ وَهُوَ صَغِيرُ السِّنِّ', |
||||
|
'translation': 'Он отвечал на вопросы ученых в юном возрасте', |
||||
|
'order': 2 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
'title': 'Пророк Мухаммад (да благословит его Аллах)', |
||||
|
'description': 'Жизнь и миссия последнего пророка Аллаха, печати пророков', |
||||
|
'categories': [categories[0], categories[3]], # Пророки, Исламская история |
||||
|
'collections': [collections[1]], |
||||
|
'content_sections': [ |
||||
|
{ |
||||
|
'title': 'Введение о Пророке', |
||||
|
'content': 'Мухаммад ибн Абдуллах - последний пророк и посланник Аллаха.\nОн родился в Мекке в 570 году н.э. в племени курайш.\nЕго послание является последним откровением для всего человечества.', |
||||
|
'priority': 1, |
||||
|
'parts': [ |
||||
|
{ |
||||
|
'order': 1, |
||||
|
'text_sections': [ |
||||
|
{ |
||||
|
'arabic_text': 'مُحَمَّدٌ رَسُولُ اللَّهِ وَخَاتَمُ النَّبِيِّينَ', |
||||
|
'translation': 'Мухаммад - посланник Аллаха и печать пророков', |
||||
|
'order': 1 |
||||
|
}, |
||||
|
{ |
||||
|
'arabic_text': 'وَمَا أَرْسَلْنَاكَ إِلَّا رَحْمَةً لِّلْعَالَمِينَ', |
||||
|
'translation': 'Мы послали тебя только как милость для миров', |
||||
|
'order': 2 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
'title': 'Начало откровения', |
||||
|
'content': 'В возрасте сорока лет пророк Мухаммад получил первое откровение в пещере Хира.\nАнгел Джибриль (Гавриил) явился ему с первыми аятами Корана.\nЭто было началом его пророческой миссии, которая продлилась 23 года.', |
||||
|
'priority': 2, |
||||
|
'parts': [ |
||||
|
{ |
||||
|
'order': 1, |
||||
|
'text_sections': [ |
||||
|
{ |
||||
|
'arabic_text': 'اقْرَأْ بِاسْمِ رَبِّكَ الَّذِي خَلَقَ', |
||||
|
'translation': 'Читай во имя Господа твоего, Который сотворил', |
||||
|
'order': 1 |
||||
|
}, |
||||
|
{ |
||||
|
'arabic_text': 'خَلَقَ الْإِنسَانَ مِنْ عَلَقٍ', |
||||
|
'translation': 'Сотворил человека из сгустка', |
||||
|
'order': 2 |
||||
|
}, |
||||
|
{ |
||||
|
'arabic_text': 'اقْرَأْ وَرَبُّكَ الْأَكْرَمُ', |
||||
|
'translation': 'Читай, ведь твой Господь - Самый великодушный', |
||||
|
'order': 3 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
'title': 'Имам Али (мир ему)', |
||||
|
'description': 'Первый имам и двоюродный брат пророка Мухаммада, врата знания', |
||||
|
'categories': [categories[1], categories[2]], # Имамы, Жизнь имамов |
||||
|
'collections': [collections[0]], |
||||
|
'content_sections': [ |
||||
|
{ |
||||
|
'title': 'Али - врата знания', |
||||
|
'content': 'Имам Али ибн Абу Талиб - первый имам шиитов и четвертый праведный халиф.\nОн был двоюродным братом и зятем пророка Мухаммада.\nПророк сказал о нем: "Я - город знания, а Али - его врата".', |
||||
|
'priority': 1, |
||||
|
'parts': [ |
||||
|
{ |
||||
|
'order': 1, |
||||
|
'text_sections': [ |
||||
|
{ |
||||
|
'arabic_text': 'عَلِيٌّ مَعَ الْحَقِّ وَالْحَقُّ مَعَ عَلِيٍّ', |
||||
|
'translation': 'Али с истиной, и истина с Али', |
||||
|
'order': 1 |
||||
|
}, |
||||
|
{ |
||||
|
'arabic_text': 'أَنَا مَدِينَةُ الْعِلْمِ وَعَلِيٌّ بَابُهَا', |
||||
|
'translation': 'Я - город знания, а Али - его врата', |
||||
|
'order': 2 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
'title': 'Мудрость имама Али', |
||||
|
'content': 'Имам Али известен своими мудрыми изречениями и наставлениями.\nЕго книга "Нахдж аль-Балага" содержит его проповеди, письма и изречения.\nОн был образцом справедливости, храбрости и знания.', |
||||
|
'priority': 2, |
||||
|
'parts': [ |
||||
|
{ |
||||
|
'order': 1, |
||||
|
'text_sections': [ |
||||
|
{ |
||||
|
'arabic_text': 'النَّاسُ أَعْدَاءُ مَا جَهِلُوا', |
||||
|
'translation': 'Люди - враги того, чего они не знают', |
||||
|
'order': 1 |
||||
|
}, |
||||
|
{ |
||||
|
'arabic_text': 'الْعِلْمُ خَيْرُ مِنَ الْمَالِ', |
||||
|
'translation': 'Знание лучше, чем богатство', |
||||
|
'order': 2 |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
'order': 2, |
||||
|
'text_sections': [ |
||||
|
{ |
||||
|
'arabic_text': 'قِيمَةُ كُلِّ امْرِئٍ مَا يُحْسِنُهُ', |
||||
|
'translation': 'Ценность каждого человека в том, что он хорошо делает', |
||||
|
'order': 1 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
'title': 'Имам Хусейн (мир ему)', |
||||
|
'description': 'Третий имам и внук пророка Мухаммада, мученик Кербелы', |
||||
|
'categories': [categories[1], categories[2]], |
||||
|
'collections': [collections[0]], |
||||
|
'content_sections': [ |
||||
|
{ |
||||
|
'title': 'Жертвоприношение в Кербеле', |
||||
|
'content': 'Имам Хусейн ибн Али - третий имам шиитов и внук пророка Мухаммада.\nОн принял мученичество в Кербеле в 680 году н.э., защищая истину и справедливость.\nЕго жертва стала символом борьбы против тирании и несправедливости.', |
||||
|
'priority': 1, |
||||
|
'parts': [ |
||||
|
{ |
||||
|
'order': 1, |
||||
|
'text_sections': [ |
||||
|
{ |
||||
|
'arabic_text': 'إِنِّي لَا أَرَى الْمَوْتَ إِلَّا سَعَادَةً وَالْحَيَاةَ مَعَ الظَّالِمِينَ إِلَّا بَرَمًا', |
||||
|
'translation': 'Я не вижу смерть иначе как счастье, а жизнь с угнетателями - иначе как несчастье', |
||||
|
'order': 1 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
'title': 'Послание Кербелы', |
||||
|
'content': 'Восстание имама Хусейна не было военным восстанием, а духовной революцией.\nОн выступил против несправедливости и коррупции правителя Язида.\nЕго послание остается актуальным для всех поколений: "Если у вас нет религии, то хотя бы будьте свободными".', |
||||
|
'priority': 2, |
||||
|
'parts': [ |
||||
|
{ |
||||
|
'order': 1, |
||||
|
'text_sections': [ |
||||
|
{ |
||||
|
'arabic_text': 'هَيْهَاتَ مِنَّا الذِّلَّةُ', |
||||
|
'translation': 'Далеки мы от унижения', |
||||
|
'order': 1 |
||||
|
}, |
||||
|
{ |
||||
|
'arabic_text': 'مَوْتٌ فِي عِزٍّ خَيْرٌ مِنْ حَيَاةٍ فِي ذُلٍّ', |
||||
|
'translation': 'Смерть в достоинстве лучше жизни в унижении', |
||||
|
'order': 2 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
'title': 'Пророк Иса (мир ему)', |
||||
|
'description': 'Иисус, сын Марии, один из великих пророков в исламе', |
||||
|
'categories': [categories[0]], |
||||
|
'collections': [collections[1]], |
||||
|
'content_sections': [ |
||||
|
{ |
||||
|
'title': 'Пророк Иса в исламе', |
||||
|
'content': 'Иса ибн Марьям (Иисус) - один из величайших пророков в исламе.\nОн был рожден чудесным образом от девы Марии по воле Аллаха.\nМусульмане почитают его как пророка и посланника Бога, но не как Бога.', |
||||
|
'priority': 1, |
||||
|
'parts': [ |
||||
|
{ |
||||
|
'order': 1, |
||||
|
'text_sections': [ |
||||
|
{ |
||||
|
'arabic_text': 'إِنَّ مَثَلَ عِيسَىٰ عِندَ اللَّهِ كَمَثَلِ آدَمَ ۖ خَلَقَهُ مِن تُرَابٍ ثُمَّ قَالَ لَهُ كُن فَيَكُونُ', |
||||
|
'translation': 'Воистину, Иса перед Аллахом подобен Адаму. Он сотворил его из праха, потом сказал ему: "Будь!" - и тот возник', |
||||
|
'order': 1 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
}, |
||||
|
{ |
||||
|
'title': 'Чудеса пророка Исы', |
||||
|
'content': 'Аллах даровал пророку Исе множество чудес в подтверждение его пророчества.\nОн исцелял больных, воскрешал мертвых и говорил с людьми еще в колыбели.\nВсе эти чудеса происходили по воле и дозволению Аллаха.', |
||||
|
'priority': 2, |
||||
|
'parts': [ |
||||
|
{ |
||||
|
'order': 1, |
||||
|
'text_sections': [ |
||||
|
{ |
||||
|
'arabic_text': 'وَأُبْرِئُ الْأَكْمَهَ وَالْأَبْرَصَ وَأُحْيِي الْمَوْتَىٰ بِإِذْنِ اللَّهِ', |
||||
|
'translation': 'Я исцеляю слепого и прокаженного и оживляю мертвых с дозволения Аллаха', |
||||
|
'order': 1 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
|
||||
|
articles = [] |
||||
|
for article_data in articles_data: |
||||
|
# Create or get article |
||||
|
article, created = Article.objects.get_or_create( |
||||
|
title=article_data['title'], |
||||
|
defaults={ |
||||
|
'slug': slugify(article_data['title'], allow_unicode=True), |
||||
|
'description': article_data['description'], |
||||
|
'status': True |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
if created: |
||||
|
self.stdout.write(f' Created article: {article.title}') |
||||
|
|
||||
|
# Add categories |
||||
|
article.categories.set(article_data['categories']) |
||||
|
|
||||
|
# Add to collections |
||||
|
for collection in article_data['collections']: |
||||
|
from apps.article.models import ArticleInCollection |
||||
|
ArticleInCollection.objects.get_or_create( |
||||
|
collection=collection, |
||||
|
article=article, |
||||
|
defaults={'order': 1} |
||||
|
) |
||||
|
|
||||
|
# Create content sections |
||||
|
for content_data in article_data['content_sections']: |
||||
|
article_content = ArticleContent.objects.create( |
||||
|
article=article, |
||||
|
title=content_data['title'], |
||||
|
content=content_data['content'], |
||||
|
priority=content_data['priority'], |
||||
|
status=True |
||||
|
) |
||||
|
|
||||
|
# Create parts |
||||
|
for part_data in content_data['parts']: |
||||
|
content_part = ContentPart.objects.create( |
||||
|
article_content=article_content, |
||||
|
order=part_data['order'] |
||||
|
) |
||||
|
|
||||
|
# Create text sections |
||||
|
for section_data in part_data['text_sections']: |
||||
|
TextSection.objects.create( |
||||
|
content_part=content_part, |
||||
|
arabic_text=section_data['arabic_text'], |
||||
|
translation=section_data['translation'], |
||||
|
order=section_data['order'] |
||||
|
) |
||||
|
|
||||
|
articles.append(article) |
||||
|
|
||||
|
return articles |
||||
@ -0,0 +1,47 @@ |
|||||
|
# Generated by Django 5.1.8 on 2025-12-02 16:17 |
||||
|
|
||||
|
import django.db.models.deletion |
||||
|
from django.db import migrations, models |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('article', '0002_article_download_count'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AlterModelOptions( |
||||
|
name='middlearticlecollection', |
||||
|
options={'verbose_name': 'Regular Collection (Middle Section)', 'verbose_name_plural': 'Regular Collections (Middle Section)'}, |
||||
|
), |
||||
|
migrations.AlterModelOptions( |
||||
|
name='pinnedarticlecollection', |
||||
|
options={'verbose_name': 'Pinned Collection (Top Section)', 'verbose_name_plural': 'Pinned Collections (Top Section)'}, |
||||
|
), |
||||
|
migrations.RemoveField( |
||||
|
model_name='contentpart', |
||||
|
name='arabic_text', |
||||
|
), |
||||
|
migrations.RemoveField( |
||||
|
model_name='contentpart', |
||||
|
name='translation', |
||||
|
), |
||||
|
migrations.CreateModel( |
||||
|
name='TextSection', |
||||
|
fields=[ |
||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
|
('arabic_text', models.TextField(verbose_name='Arabic text')), |
||||
|
('translation', models.TextField(verbose_name='Translation')), |
||||
|
('order', models.PositiveIntegerField(default=0, verbose_name='order')), |
||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), |
||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')), |
||||
|
('content_part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='text_sections', to='article.contentpart', verbose_name='content part')), |
||||
|
], |
||||
|
options={ |
||||
|
'verbose_name': 'Text Section', |
||||
|
'verbose_name_plural': 'Text Sections', |
||||
|
'ordering': ['order'], |
||||
|
}, |
||||
|
), |
||||
|
] |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue