From f0666319a3acf585896cbd2d742286a6030c0c25 Mon Sep 17 00:00:00 2001 From: mortezaei Date: Tue, 2 Dec 2025 17:19:14 +0330 Subject: [PATCH] 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. --- apps/api/views/documentation.py | 191 ++++++++ apps/article/admin.py | 62 ++- apps/article/management/__init__.py | 0 apps/article/management/commands/__init__.py | 0 .../management/commands/seed_article_data.py | 445 ++++++++++++++++++ ...iddlearticlecollection_options_and_more.py | 47 ++ apps/article/models.py | 36 +- apps/article/serializers.py | 13 +- apps/hadis/models/category.py | 1 + 9 files changed, 774 insertions(+), 21 deletions(-) create mode 100644 apps/article/management/__init__.py create mode 100644 apps/article/management/commands/__init__.py create mode 100644 apps/article/management/commands/seed_article_data.py create mode 100644 apps/article/migrations/0003_alter_middlearticlecollection_options_and_more.py diff --git a/apps/api/views/documentation.py b/apps/api/views/documentation.py index 4503f08..3c06908 100644 --- a/apps/api/views/documentation.py +++ b/apps/api/views/documentation.py @@ -831,5 +831,196 @@ class CustomAPIDocumentationView(View): } } ] + }, + 'article': { + 'name': 'Articles', + 'description': 'Articles with structured content including Arabic texts and translations', + 'endpoints': [ + { + 'name': 'Article Categories', + 'method': 'GET', + 'url': '/api/article/categories/', + 'description': 'Get list of all active article categories', + 'parameters': [], + 'response_examples': { + 'success': json.dumps([ + { + "id": 1, + "title": "Категория статей", + "slug": "category-1", + "acticle_count": 12 + } + ], indent=2) + } + }, + { + 'name': 'Pinned Collections', + 'method': 'GET', + 'url': '/api/article/pinned-collections/', + 'description': 'Get pinned article collections', + 'parameters': [], + 'response_examples': { + 'success': json.dumps({ + "count": 3, + "info": { + "categories_count": 4, + "bookmarks_count": 12 + }, + "results": [ + { + "id": 1, + "title": "Избранные статьи", + "slug": "featured-articles", + "summary": "Лучшие статьи", + "thumbnail": "https://example.com/media/collections/thumb1.jpg", + "order": 1, + "created_at": "2025-01-15T10:30:00Z" + } + ] + }, indent=2) + } + }, + { + 'name': 'Article Collections', + 'method': 'GET', + 'url': '/api/article/collections/', + 'description': 'Get article collections with articles', + 'parameters': [], + 'response_examples': { + 'success': json.dumps({ + "count": 5, + "results": [ + { + "id": 1, + "title": "Коллекция статей об имамах", + "slug": "imams-collection", + "summary": "Статьи о жизни имамов", + "articles": [ + { + "id": 1, + "title": "Имам Джавад (мир ему)", + "slug": "imam-javad", + "thumbnail": "https://example.com/media/articles/thumb1.jpg", + "description": "Краткая биография", + "view_count": 234, + "created_at": "2025-01-15T10:30:00Z" + } + ] + } + ] + }, indent=2) + } + }, + { + 'name': 'Article List', + 'method': 'GET', + 'url': '/api/article/list/', + 'description': 'Get paginated list of articles with filtering', + 'parameters': [ + {'name': 'category', 'type': 'string', 'description': 'Filter by category slug', 'required': False}, + {'name': 'collection', 'type': 'string', 'description': 'Filter by collection slug', 'required': False}, + {'name': 'is_bookmark', 'type': 'boolean', 'description': 'Filter bookmarked articles', 'required': False}, + {'name': 'search', 'type': 'string', 'description': 'Search in article titles', 'required': False}, + ], + 'response_examples': { + 'success': json.dumps({ + "count": 45, + "results": [ + { + "id": 1, + "title": "Имам Джавад (мир ему)", + "slug": "imam-javad", + "thumbnail": "https://example.com/media/articles/thumb1.jpg", + "description": "Краткая биография девятого имама", + "view_count": 234, + "created_at": "2025-01-15T10:30:00Z" + } + ] + }, indent=2) + } + }, + { + 'name': 'Article Detail', + 'method': 'GET', + 'url': '/api/article/detail//', + 'description': 'Get detailed article with structured content including Arabic texts and translations', + 'parameters': [ + {'name': 'slug', 'type': 'string', 'description': 'Article slug', 'required': True}, + ], + 'response_examples': { + 'success': json.dumps({ + "id": 1, + "title": "Имам Джавад (мир ему)", + "slug": "imam-javad", + "thumbnail": "https://example.com/media/articles/thumb1.jpg", + "description": "Краткая биография", + "article_file": "https://example.com/media/articles/document.pdf", + "view_count": 235, + "download_count": 45, + "categories": [ + { + "id": 1, + "title": "Имамы", + "slug": "imams", + "acticle_count": 12 + } + ], + "created_at": "2025-01-15T10:30:00Z", + "user_rate": { + "is_rated": True, + "rate": 5 + }, + "average_rate": { + "average": 4.5, + "count": 23 + }, + "bookmark": True, + "article_content": [ + { + "id": 1, + "title": "Введение", + "content": "Краткое введение о теме", + "priority": 1, + "status": True, + "created_at": "2025-01-15T10:30:00Z", + "updated_at": "2025-01-15T10:30:00Z", + "parts": [ + { + "id": 1, + "order": 1, + "text_sections": [ + { + "id": 1, + "arabic_text": "بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ", + "translation": "Во имя Аллаха, Милостивого, Милосердного", + "order": 1 + }, + { + "id": 2, + "arabic_text": "الْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ", + "translation": "Хвала Аллаху, Господу миров", + "order": 2 + } + ] + }, + { + "id": 2, + "order": 2, + "text_sections": [ + { + "id": 3, + "arabic_text": "الرَّحْمَٰنِ الرَّحِيمِ", + "translation": "Милостивому, Милосердному", + "order": 1 + } + ] + } + ] + } + ] + }, indent=2) + } + } + ] } } diff --git a/apps/article/admin.py b/apps/article/admin.py index 73d5924..6f472fd 100755 --- a/apps/article/admin.py +++ b/apps/article/admin.py @@ -17,14 +17,15 @@ from utils.admin import project_admin_site from unfold.sections import TableSection from apps.article.models import ( - ArticleCategory, - ArticleCollection, - PinnedArticleCollection, - MiddleArticleCollection, - Article, + ArticleCategory, + ArticleCollection, + PinnedArticleCollection, + MiddleArticleCollection, + Article, ArticleInCollection, ArticleContent, - ContentPart + ContentPart, + TextSection ) @@ -39,13 +40,22 @@ class ArticleInCollectionInline(TabularInline): tab = True +class TextSectionInline(TabularInline): + model = TextSection + extra = 1 + fields = ('arabic_text', 'translation', 'order') + ordering = ('order',) + verbose_name = _('Text Section') + verbose_name_plural = _('Text Sections') + class ContentPartInline(StackedInline): model = ContentPart extra = 1 - fields = ('arabic_text', 'translation', 'order') + fields = ('order',) ordering = ('order',) verbose_name = _('Content Part') verbose_name_plural = _('Content Parts') + tab = True class ArticleContentInline(StackedInline): @@ -254,22 +264,49 @@ class ArticleContentAdmin(ModelAdmin): search_fields = ('title', 'content') autocomplete_fields = ('article',) inlines = [ContentPartInline] - + actions_row = [ + "action_parts", + ] + fieldsets = ( (None, { 'fields': ('article', 'title', 'content', 'priority', 'status') }), ) - + def get_changeform_initial_data(self, request): initial = super().get_changeform_initial_data(request) if 'article__id__exact' in request.GET: initial['article'] = request.GET.get('article__id__exact') return initial - # @display(description=_("Content Title"), header=True) - # def display_header(self, obj): - # return str(obj.title) + @action( + description=_("Parts"), + url_path="actions-row-parts-url", + ) + def action_parts(self, request, object_id): + content = get_object_or_404(ArticleContent, pk=object_id) + url = reverse('admin:article_contentpart_changelist') + f'?article_content__id__exact={content.id}' + return redirect(url) + + +class ContentPartAdmin(ModelAdmin): + list_display = ('article_content', 'order', 'created_at') + list_filter = ('created_at',) + autocomplete_fields = ('article_content',) + inlines = [TextSectionInline] + + fieldsets = ( + (None, { + 'fields': ('article_content', 'order') + }), + ) + + def get_changeform_initial_data(self, request): + initial = super().get_changeform_initial_data(request) + if 'article_content__id__exact' in request.GET: + initial['article_content'] = request.GET.get('article_content__id__exact') + return initial # Register models with admin site @@ -278,3 +315,4 @@ project_admin_site.register(PinnedArticleCollection, PinnedArticleCollectionAdmi project_admin_site.register(MiddleArticleCollection, MiddleArticleCollectionAdmin) project_admin_site.register(Article, ArticleAdmin) project_admin_site.register(ArticleContent, ArticleContentAdmin) +project_admin_site.register(ContentPart, ContentPartAdmin) diff --git a/apps/article/management/__init__.py b/apps/article/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/article/management/commands/__init__.py b/apps/article/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/article/management/commands/seed_article_data.py b/apps/article/management/commands/seed_article_data.py new file mode 100644 index 0000000..e0229bf --- /dev/null +++ b/apps/article/management/commands/seed_article_data.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 diff --git a/apps/article/migrations/0003_alter_middlearticlecollection_options_and_more.py b/apps/article/migrations/0003_alter_middlearticlecollection_options_and_more.py new file mode 100644 index 0000000..e99eab8 --- /dev/null +++ b/apps/article/migrations/0003_alter_middlearticlecollection_options_and_more.py @@ -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'], + }, + ), + ] diff --git a/apps/article/models.py b/apps/article/models.py index f9043b7..28a065b 100755 --- a/apps/article/models.py +++ b/apps/article/models.py @@ -184,7 +184,7 @@ class ArticleContent(models.Model): class ContentPart(models.Model): """ - Model for bilingual content parts (Arabic text and translation) + Model for content parts - each part can have multiple text sections """ article_content = models.ForeignKey( ArticleContent, @@ -192,20 +192,44 @@ class ContentPart(models.Model): related_name='parts', verbose_name=_('article content') ) - 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')) - + class Meta: verbose_name = _('Content Part') verbose_name_plural = _('Content Parts') ordering = ['order'] - + def __str__(self): return f"{self.article_content.title} - Part {self.order}" +class TextSection(models.Model): + """ + Model for bilingual text sections (Arabic text and translation) within a content part + """ + content_part = models.ForeignKey( + ContentPart, + on_delete=models.CASCADE, + related_name='text_sections', + verbose_name=_('content part') + ) + 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')) + + class Meta: + verbose_name = _('Text Section') + verbose_name_plural = _('Text Sections') + ordering = ['order'] + + def __str__(self): + return f"{self.content_part} - Section {self.order}" + + diff --git a/apps/article/serializers.py b/apps/article/serializers.py index f912686..155559e 100644 --- a/apps/article/serializers.py +++ b/apps/article/serializers.py @@ -52,13 +52,20 @@ class ArticleListSerializer(serializers.ModelSerializer): return get_thumbs(obj.thumbnail, self.context.get('request')) +class TextSectionSerializer(serializers.ModelSerializer): + class Meta: + model = TextSection + fields = ['id', 'arabic_text', 'translation', 'order'] + class ContentPartSerializer(serializers.ModelSerializer): + text_sections = TextSectionSerializer(many=True, read_only=True) + class Meta: model = ContentPart - fields = ['id', 'arabic_text', 'translation', 'order', 'created_at', 'updated_at'] - + fields = ['id', 'order', 'text_sections'] + class ArticleContentSerializer(serializers.ModelSerializer): - parts = ContentPartSerializer(many=True, read_only=True) + parts = ContentPartSerializer(many=True, read_only=True) class Meta: model = ArticleContent fields = ['id', 'title', 'content', 'priority', 'status', 'created_at', 'updated_at', 'parts'] diff --git a/apps/hadis/models/category.py b/apps/hadis/models/category.py index 70189cf..5b107ce 100644 --- a/apps/hadis/models/category.py +++ b/apps/hadis/models/category.py @@ -11,6 +11,7 @@ class HadisSect(models.Model): sect_type = models.CharField(max_length=10, choices=SectType.choices, unique=True, verbose_name=_('Sect Name')) title = models.CharField(max_length=256, verbose_name=_('Name')) + description = models.TextField(verbose_name=_('Description'), null=True, blank=True) is_active = models.BooleanField(default=True, verbose_name=_('Is Active')) order = models.IntegerField(default=0, verbose_name=_('order'))