From b20ea09148a3c658618ea22a21ed97523d6dc027 Mon Sep 17 00:00:00 2001 From: mortezaei Date: Fri, 21 Nov 2025 03:14:25 +0330 Subject: [PATCH] feat(collections): enhance article and podcast admin interfaces with display position - Updated `ArticleCollectionAdminBase` and `PodcastCollectionAdminBase` to include a new `get_display_position` method for better visual distinction between pinned and regular collections. - Modified verbose names for `PinnedArticleCollection` and `MiddleArticleCollection` to clarify their purpose in the admin interface. - Added a new README for podcast collections to provide guidance on managing collections and highlight key differences between pinned and regular collections. - Updated settings to improve sidebar navigation for article and podcast collections, ensuring clear access to both collection types. --- apps/article/admin.py | 9 ++- apps/article/models.py | 8 +- apps/podcast/README_COLLECTIONS.md | 120 +++++++++++++++++++++++++++++ apps/podcast/admin.py | 22 +++++- apps/podcast/models.py | 8 +- config/settings/base.py | 54 ++++++++++++- 6 files changed, 206 insertions(+), 15 deletions(-) create mode 100644 apps/podcast/README_COLLECTIONS.md diff --git a/apps/article/admin.py b/apps/article/admin.py index f25c0d1..73d5924 100755 --- a/apps/article/admin.py +++ b/apps/article/admin.py @@ -59,7 +59,7 @@ class ArticleContentInline(StackedInline): class ArticleCollectionAdminBase(ModelAdmin): - list_display = ('get_title', 'status', 'order', 'count_articles') + list_display = ('get_title', 'get_display_position', 'status', 'order', 'count_articles') list_filter = ('status', 'order') search_fields = ('title',) ordering = ('order',) @@ -81,6 +81,13 @@ class ArticleCollectionAdminBase(ModelAdmin): def get_title(self, obj): return str(obj.title) + @display(description=_('Display Position')) + def get_display_position(self, obj): + if obj.display_position == ArticleCollection.DisplayPosition.PINNED: + return format_html('📌 Pinned (Top)') + else: + return format_html('📋 Regular (Middle)') + @display(description=_('Number of Articles')) def count_articles(self, obj): count = obj.related_articles.count() diff --git a/apps/article/models.py b/apps/article/models.py index 91cf186..f9043b7 100755 --- a/apps/article/models.py +++ b/apps/article/models.py @@ -71,15 +71,15 @@ class ArticleCollection(models.Model): class PinnedArticleCollection(ArticleCollection): class Meta: proxy = True - verbose_name = _('Pinned Article Collection') - verbose_name_plural = _('Pinned Article Collections') + verbose_name = _('Pinned Collection (Top Section)') + verbose_name_plural = _('Pinned Collections (Top Section)') class MiddleArticleCollection(ArticleCollection): class Meta: proxy = True - verbose_name = _('Middle Section Article Collection') - verbose_name_plural = _('Middle Section Article Collections') + verbose_name = _('Regular Collection (Middle Section)') + verbose_name_plural = _('Regular Collections (Middle Section)') class Article(models.Model): diff --git a/apps/podcast/README_COLLECTIONS.md b/apps/podcast/README_COLLECTIONS.md new file mode 100644 index 0000000..5117ef2 --- /dev/null +++ b/apps/podcast/README_COLLECTIONS.md @@ -0,0 +1,120 @@ +# مستندات مدیریت کالکشن‌های پادکست + +## نحوه مدیریت کالکشن‌ها در پنل ادمین + +### دو نوع کالکشن داریم: + +#### 1️⃣ **Pinned Collections (کالکشن‌های پین‌شده - بخش بالا)** +- **مسیر در پنل ادمین:** `Pinned Collections (Top Section)` +- **API Endpoint:** `/api/podcast/pinned-collections/` +- **کاربرد:** نمایش در بالای صفحه (carousel/featured section) +- **ویژگی‌ها:** + - نیاز به تصویر thumbnail دارد + - می‌تواند summary داشته باشد + - ترتیب نمایش با فیلد `order` مشخص می‌شود + +#### 2️⃣ **Regular Collections (کالکشن‌های معمولی - بخش میانی)** +- **مسیر در پنل ادمین:** `Regular Collections (Middle Section)` +- **API Endpoint:** `/api/podcast/collections/` +- **کاربرد:** نمایش در بخش‌های میانی صفحه +- **ویژگی‌ها:** + - تصویر thumbnail اختیاری است + - ترتیب نمایش با فیلد `order` مشخص می‌شود + +--- + +## راهنمای استفاده + +### ایجاد کالکشن جدید + +**برای کالکشن پین‌شده (بالای صفحه):** +1. به بخش `Pinned Collections (Top Section)` بروید +2. روی "Add" کلیک کنید +3. فیلدهای زیر را پر کنید: + - Title (عنوان) + - Summary (خلاصه - اختیاری) + - Thumbnail (تصویر - **الزامی**) + - Order (ترتیب نمایش) + - Status (فعال/غیرفعال) +4. پادکست‌های مورد نظر را اضافه کنید + +**برای کالکشن معمولی (بخش میانی):** +1. به بخش `Regular Collections (Middle Section)` بروید +2. روی "Add" کلیک کنید +3. فیلدهای زیر را پر کنید: + - Title (عنوان) + - Order (ترتیب نمایش) + - Status (فعال/غیرفعال) +4. پادکست‌های مورد نظر را اضافه کنید + +--- + +## نکات مهم + +### تشخیص نوع کالکشن +در لیست کالکشن‌ها، ستون **Display Position** نوع هر کالکشن را نشان می‌دهد: +- 📌 **Pinned (Top)** → کالکشن پین‌شده +- 📋 **Regular (Middle)** → کالکشن معمولی + +### تفاوت‌های کلیدی + +| ویژگی | Pinned | Regular | +|-------|--------|---------| +| تصویر thumbnail | ✅ الزامی | ⚪ اختیاری | +| فیلد summary | ✅ دارد | ❌ ندارد | +| محل نمایش | بالای صفحه | بخش میانی | +| API Endpoint | `/pinned-collections/` | `/collections/` | + +--- + +## ساختار فنی + +### مدل‌ها +```python +# مدل پایه +PodcastCollection +├── display_position: 'pinned' یا 'middle' +├── title +├── slug +├── summary (nullable) +├── thumbnail (nullable) +├── order +└── status + +# مدل‌های Proxy +PinnedPodcastCollection (display_position='pinned') +MiddlePodcastCollection (display_position='middle') +``` + +### فیلد display_position +این فیلد به صورت خودکار توسط Django Admin تنظیم می‌شود: +- در `Pinned Collections` → `display_position='pinned'` +- در `Regular Collections` → `display_position='middle'` + +--- + +## سوالات متداول + +**Q: چرا دو بخش جدا داریم؟** +A: برای جلوگیری از اشتباه و مدیریت بهتر. هر کدام کاربرد و ویژگی‌های متفاوتی دارند. + +**Q: می‌توانم یک کالکشن را از Pinned به Regular تبدیل کنم؟** +A: خیر، باید کالکشن جدیدی در بخش مورد نظر ایجاد کنید و پادکست‌ها را کپی کنید. + +**Q: چرا در API دو endpoint جدا داریم؟** +A: چون frontend نیاز دارد که کالکشن‌های بالا و میانی را جداگانه دریافت کند. + +--- + +## تغییرات اخیر + +### نسخه جدید (بهبود UX) +- ✅ نام‌های واضح‌تر برای مدل‌ها +- ✅ نمایش `Display Position` در لیست +- ✅ آیکون‌های بصری برای تشخیص سریع‌تر +- ✅ مستندات کامل + +### نسخه قبلی +- نام‌های مبهم (`Middle Section` به جای `Regular`) +- عدم نمایش نوع کالکشن در لیست +- سردرگمی در یافتن بخش مناسب \ No newline at end of file diff --git a/apps/podcast/admin.py b/apps/podcast/admin.py index 88e41c5..413c21b 100755 --- a/apps/podcast/admin.py +++ b/apps/podcast/admin.py @@ -27,7 +27,7 @@ class PodcastInCollectionInline(TabularInline): class PodcastCollectionAdminBase(ModelAdmin): - list_display = ('get_title', 'status', 'order', 'count_podcasts') + list_display = ('get_title', 'get_display_position', 'status', 'order', 'count_podcasts') list_filter = ('status', 'order') search_fields = ('title',) ordering = ('order',) @@ -49,6 +49,13 @@ class PodcastCollectionAdminBase(ModelAdmin): def get_title(self, obj): return str(obj.title) + @display(description=_('Display Position')) + def get_display_position(self, obj): + if obj.display_position == PodcastCollection.DisplayPosition.PINNED: + return format_html('📌 Pinned (Top)') + else: + return format_html('📋 Regular (Middle)') + @display(description=_('Number of Podcasts')) def count_podcasts(self, obj): count = obj.related_podcasts.count() @@ -70,6 +77,12 @@ class PinnedPodcastCollectionForm(forms.ModelForm): class PinnedPodcastCollectionAdmin(PodcastCollectionAdminBase): form = PinnedPodcastCollectionForm + # Add help text to clarify this is for top section + class Media: + css = { + 'all': () + } + def get_queryset(self, request): return super().get_queryset(request).filter(display_position=PodcastCollection.DisplayPosition.PINNED) @@ -77,7 +90,6 @@ class PinnedPodcastCollectionAdmin(PodcastCollectionAdminBase): obj.display_position = PodcastCollection.DisplayPosition.PINNED super().save_model(request, obj, form, change) - @display(description=_('Title')) def get_title(self, obj): from django.templatetags.static import static @@ -91,6 +103,12 @@ class MiddlePodcastCollectionAdmin(PodcastCollectionAdminBase): 'fields': ('title', 'status', 'pin_top', 'order') }), ) + + # Add help text to clarify this is for middle section + class Media: + css = { + 'all': () + } def get_queryset(self, request): return super().get_queryset(request).filter(display_position=PodcastCollection.DisplayPosition.MIDDLE) diff --git a/apps/podcast/models.py b/apps/podcast/models.py index be606b0..a505e23 100755 --- a/apps/podcast/models.py +++ b/apps/podcast/models.py @@ -71,15 +71,15 @@ class PodcastCollection(models.Model): class PinnedPodcastCollection(PodcastCollection): class Meta: proxy = True - verbose_name = _('Pinned Podcast Collection') - verbose_name_plural = _('Pinned Podcast Collections') + verbose_name = _('Pinned Collection (Top Section)') + verbose_name_plural = _('Pinned Collections (Top Section)') class MiddlePodcastCollection(PodcastCollection): class Meta: proxy = True - verbose_name = _('Middle Section Podcast Collection') - verbose_name_plural = _('Middle Section Podcast Collections') + verbose_name = _('Regular Collection (Middle Section)') + verbose_name_plural = _('Regular Collections (Middle Section)') diff --git a/config/settings/base.py b/config/settings/base.py index b6b70be..955ccf4 100755 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -431,6 +431,24 @@ UNFOLD = { }, ], }, + { + "page": "article", + "models": ["article.articlecollection", "article.pinnedarticlecollection", "article.middlearticlecollection"], + "items": [ + { + "title": _("Pinned Collections"), + "icon": "collections_bookmark", + "link": reverse_lazy("admin:article_pinnedarticlecollection_changelist"), + "active": lambda request: "article/pinnedarticlecollection" in request.path and "article/middlearticlecollection" not in request.path, + }, + { + "title": _("Regular Collections"), + "icon": "view_module", + "link": reverse_lazy("admin:article_middlearticlecollection_changelist"), + "active": lambda request: "article/middlearticlecollection" in request.path, + }, + ], + }, { "page": "accounts", "models": ["account.user", 'auth.group'], @@ -526,6 +544,24 @@ UNFOLD = { }, ], }, + { + "page": "podcast", + "models": ["podcast.podcastcollection", "podcast.pinnedpodcastcollection", "podcast.middlepodcastcollection"], + "items": [ + { + "title": _("Pinned Collections"), + "icon": "collections_bookmark", + "link": reverse_lazy("admin:podcast_pinnedpodcastcollection_changelist"), + "active": lambda request: "podcast/pinnedpodcastcollection" in request.path and "podcast/middlepodcastcollection" not in request.path, + }, + { + "title": _("Regular Collections"), + "icon": "view_module", + "link": reverse_lazy("admin:podcast_middlepodcastcollection_changelist"), + "active": lambda request: "podcast/middlepodcastcollection" in request.path, + }, + ], + }, ], "SIDEBAR": { "show_search": True, @@ -775,10 +811,15 @@ UNFOLD = { "link": reverse_lazy("admin:article_articlecategory_changelist"), }, { - "title": _("Collections"), - "icon": "view_module", + "title": _("Pinned Collections"), + "icon": "collections_bookmark", "link": reverse_lazy("admin:article_pinnedarticlecollection_changelist"), }, + { + "title": _("Regular Collections"), + "icon": "view_module", + "link": reverse_lazy("admin:article_middlearticlecollection_changelist"), + }, { "title": _("Article Contents"), "icon": "text_snippet", @@ -802,10 +843,15 @@ UNFOLD = { "link": reverse_lazy("admin:podcast_podcastcategory_changelist"), }, { - "title": _("Collections"), - "icon": "view_module", + "title": _("Pinned Collections"), + "icon": "collections_bookmark", "link": reverse_lazy("admin:podcast_pinnedpodcastcollection_changelist"), }, + { + "title": _("Regular Collections"), + "icon": "view_module", + "link": reverse_lazy("admin:podcast_middlepodcastcollection_changelist"), + }, { "title": _("Playlists"), "icon": "playlist_play",