Browse Source

fix(article, library): fix article admin slug generation, detail unicode url routing, and category active books count

- Make slug optional in AdminArticle serializers with auto-generation fallback
- Support unicode and URL-encoded slugs with case-insensitive lookup in ArticleDetailAPIView
- Count only active books (status=True) in library Category.books_count and CategoryListView
master
Mohsen Taba 5 hours ago
parent
commit
2a8cf66c8a
  1. 10
      apps/article/serializers_admin.py
  2. 5
      apps/article/urls.py
  3. 17
      apps/article/views.py
  4. 4
      apps/library/models.py
  5. 2
      apps/library/views.py

10
apps/article/serializers_admin.py

@ -39,18 +39,23 @@ class AbsoluteFileField(serializers.FileField):
class AdminArticleCategorySerializer(serializers.ModelSerializer):
slug = serializers.CharField(required=False, allow_blank=True)
class Meta:
model = ArticleCategory
fields = ["id", "title", "slug", "status", "order"]
class AdminArticleCollectionSerializer(serializers.ModelSerializer):
slug = serializers.CharField(required=False, allow_blank=True)
class Meta:
model = ArticleCollection
fields = ["id", "title", "slug", "summary", "display_position", "status", "order"]
class AdminArticleListSerializer(serializers.ModelSerializer):
slug = serializers.CharField(required=False, allow_blank=True)
thumbnail = AbsoluteImageField(required=False, allow_null=True)
article_file = AbsoluteFileField(required=False, allow_null=True)
categories = AdminArticleCategorySerializer(many=True, read_only=True)
@ -77,6 +82,7 @@ class AdminArticleListSerializer(serializers.ModelSerializer):
class AdminArticleDetailSerializer(serializers.ModelSerializer):
slug = serializers.CharField(required=False, allow_blank=True)
thumbnail = AbsoluteImageField(required=False, allow_null=True)
article_file = AbsoluteFileField(required=False, allow_null=True)
categories = serializers.PrimaryKeyRelatedField(
@ -129,6 +135,8 @@ class AdminArticleDetailSerializer(serializers.ModelSerializer):
def create(self, validated_data):
validated_data.pop("remove_thumbnail", False)
validated_data.pop("remove_article_file", False)
if not validated_data.get("slug"):
validated_data.pop("slug", None)
categories = validated_data.pop("categories", [])
collections = validated_data.pop("collections", [])
article = super().create(validated_data)
@ -141,6 +149,8 @@ class AdminArticleDetailSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
remove_thumbnail = validated_data.pop("remove_thumbnail", False)
remove_article_file = validated_data.pop("remove_article_file", False)
if not validated_data.get("slug"):
validated_data.pop("slug", None)
categories = validated_data.pop("categories", None)
collections = validated_data.pop("collections", None)

5
apps/article/urls.py

@ -24,8 +24,9 @@ urlpatterns = [
path('pinned-collections/', PinnedArticleCollectionListView.as_view(), name='pinned-collection-list'),
path('collections/', MiddleArticleCollectionListView.as_view(), name='collection-list'),
path('list/', ArticleListAPIView.as_view(), name='podcast-list'),
re_path(r'detail/(?P<slug>[\w-]+)/$', ArticleDetailAPIView.as_view(), name='podcast-detail'),
path('list/', ArticleListAPIView.as_view(), name='article-list'),
path('detail/<str:slug>/', ArticleDetailAPIView.as_view(), name='article-detail'),
re_path(r'detail/(?P<slug>.+)/$', ArticleDetailAPIView.as_view(), name='podcast-detail'),
path('admin/', include(admin_router.urls)),
# # User playlist endpoints

17
apps/article/views.py

@ -251,6 +251,23 @@ class ArticleDetailAPIView(Gone410ViewMixin, CanonicalSlugViewSetMixin, generics
def get_queryset(self):
return Article.objects.all()
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
slug_val = self.kwargs.get(self.lookup_field or 'slug')
if slug_val:
import urllib.parse
unquoted_slug = urllib.parse.unquote(slug_val)
obj = queryset.filter(
Q(slug=slug_val) |
Q(slug=unquoted_slug) |
Q(slug__iexact=slug_val) |
Q(slug__iexact=unquoted_slug)
).first()
if obj:
self.check_object_permissions(self.request, obj)
return obj
return super().get_object()
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
instance.increment_view_count()

4
apps/library/models.py

@ -77,8 +77,8 @@ class Category(LowercaseSlugMixin, models.Model):
@property
def books_count(self):
"""Return the number of books in this category"""
return self.related_categories.count()
"""Return the number of active books in this category"""
return self.related_categories.filter(status=True).count()
class Meta:
verbose_name = _('Category')

2
apps/library/views.py

@ -47,7 +47,7 @@ class CategoryListView(ListAPIView):
return Category.objects.filter(
status=True
).annotate(
books_count_annotation=Count('related_categories')
books_count_annotation=Count('related_categories', filter=Q(related_categories__status=True))
).order_by('title')

Loading…
Cancel
Save