Browse Source

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.
master
mortezaei 6 months ago
parent
commit
b20ea09148
  1. 9
      apps/article/admin.py
  2. 8
      apps/article/models.py
  3. 120
      apps/podcast/README_COLLECTIONS.md
  4. 22
      apps/podcast/admin.py
  5. 8
      apps/podcast/models.py
  6. 54
      config/settings/base.py

9
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('<span style="color: #0066cc; font-weight: bold;">📌 Pinned (Top)</span>')
else:
return format_html('<span style="color: #666;">📋 Regular (Middle)</span>')
@display(description=_('Number of Articles'))
def count_articles(self, obj):
count = obj.related_articles.count()

8
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):

120
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`)
- عدم نمایش نوع کالکشن در لیست
- سردرگمی در یافتن بخش مناسب

22
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('<span style="color: #0066cc; font-weight: bold;">📌 Pinned (Top)</span>')
else:
return format_html('<span style="color: #666;">📋 Regular (Middle)</span>')
@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
@ -92,6 +104,12 @@ class MiddlePodcastCollectionAdmin(PodcastCollectionAdminBase):
}),
)
# 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)

8
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)')

54
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",

Loading…
Cancel
Save