Skip to content

Commit 9fdf692

Browse files
author
Ahtesham Quraish
committed
add new api inside the current view set
1 parent 85472fa commit 9fdf692

File tree

6 files changed

+36
-56
lines changed

6 files changed

+36
-56
lines changed

articles/models.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,21 @@ class Article(TimestampedModel):
2727
def save(self, *args, **kwargs):
2828
previous = Article.objects.get(pk=self.pk) if self.pk else None
2929
was_published = getattr(previous, "is_published", None)
30-
if not was_published:
30+
31+
# Always initialize slug
32+
slug = self.slug or None
33+
34+
if not was_published and self.is_published:
3135
max_length = self._meta.get_field("slug").max_length
36+
3237
base_slug = slugify(self.title)[:max_length]
3338
slug = base_slug
3439
counter = 1
3540

41+
# Prevent collisions
3642
while Article.objects.filter(slug=slug).exclude(pk=self.pk).exists():
3743
suffix = f"-{counter}"
38-
slug = f"{base_slug[: 60 - len(suffix)]}{suffix}"
44+
slug = f"{base_slug[: max_length - len(suffix)]}{suffix}"
3945
counter += 1
4046

4147
self.slug = slug

articles/urls.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from rest_framework.routers import SimpleRouter
33

44
from articles import views
5-
from .views import MediaUploadView, ArticleDetailByIdOrSlugAPIView
5+
from .views import MediaUploadView
66

77
v1_router = SimpleRouter()
88
v1_router.register(
@@ -12,26 +12,21 @@
1212
)
1313

1414
app_name = "articles"
15+
1516
urlpatterns = [
1617
re_path(
1718
r"^api/v1/",
1819
include(
1920
(
2021
[
21-
# Existing router URLs for the ViewSet
22+
# All ViewSet routes
2223
*v1_router.urls,
2324
# Media upload endpoint
2425
path(
2526
"upload-media/",
2627
MediaUploadView.as_view(),
2728
name="api-media-upload",
2829
),
29-
# New endpoint: retrieve article by ID or slug
30-
path(
31-
"articles/detail/<str:identifier>/",
32-
ArticleDetailByIdOrSlugAPIView.as_view(),
33-
name="article-detail-by-id-or-slug",
34-
),
3530
],
3631
"v1",
3732
)

articles/views.py

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from rest_framework.response import Response
1313
from rest_framework.views import APIView
1414
from django.shortcuts import get_object_or_404
15+
from rest_framework.decorators import action
16+
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiResponse
1517

1618

1719
from articles.models import Article
@@ -80,45 +82,35 @@ def destroy(self, request, *args, **kwargs):
8082
clear_views_cache()
8183
return super().destroy(request, *args, **kwargs)
8284

83-
84-
class ArticleDetailByIdOrSlugAPIView(APIView):
85-
"""
86-
Retrieve an article by numeric ID or slug string.
87-
"""
88-
89-
permission_classes = [CanViewArticle] # Change if public access is allowed
90-
9185
@extend_schema(
9286
summary="Retrieve article by ID or slug",
93-
description="If the parameter is numeric, retrieve by ID. Otherwise, slug.",
87+
description="If the path parameter is numeric → ID, else → slug.",
9488
parameters=[
9589
OpenApiParameter(
9690
name="identifier",
97-
description="Article ID (number) or slug (string)",
98-
required=True,
9991
type=str,
10092
location=OpenApiParameter.PATH,
101-
),
93+
description="Article ID (number) or slug (string)",
94+
required=True,
95+
)
10296
],
103-
responses={
104-
200: RichTextArticleSerializer,
105-
404: OpenApiResponse(description="Not found"),
106-
},
97+
responses={200: RichTextArticleSerializer, 404: OpenApiResponse()},
10798
)
108-
def get(self, request, identifier):
109-
qs = Article.objects.all()
110-
111-
# Admins/staff/groups see everything
112-
if not (is_admin_user(request) or is_article_group_user(request)):
113-
qs = qs.filter(is_published=True)
99+
@action(
100+
detail=False,
101+
methods=["get"],
102+
url_path="detail/(?P<identifier>[^/.]+)",
103+
url_name="detail-by-id-or-slug",
104+
)
105+
def detail_by_id_or_slug(self, request, identifier):
106+
qs = self.get_queryset()
114107

115-
# Check if numeric → ID
116108
if identifier.isdigit():
117109
article = get_object_or_404(qs, id=int(identifier))
118110
else:
119111
article = get_object_or_404(qs, slug=identifier)
120112

121-
serializer = RichTextArticleSerializer(article, context={"request": request})
113+
serializer = self.get_serializer(article)
122114
return Response(serializer.data, status=status.HTTP_200_OK)
123115

124116

articles/views_test.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
"""Test for articles views"""
22

3-
import pytest
4-
from rest_framework.reverse import reverse
53
import pytest
64
from rest_framework.reverse import reverse
75

86
from articles.models import Article
9-
107
from main.factories import UserFactory
118

129
pytestmark = [pytest.mark.django_db]
@@ -26,16 +23,6 @@ def test_article_creation(staff_client, user):
2623
assert json["title"] == "Some title"
2724

2825

29-
@pytest.mark.parametrize("is_staff", [True, False])
30-
def test_article_permissions(client, is_staff):
31-
user = UserFactory.create(is_staff=True)
32-
client.force_login(user)
33-
url = reverse("articles:v1:articles-list")
34-
resp = client.get(url)
35-
resp.json()
36-
assert resp.status_code == 200 if is_staff else 403
37-
38-
3926
def test_retrieve_article_by_id(client, user):
4027
"""Should retrieve published article by numeric ID"""
4128
article = Article.objects.create(
@@ -46,7 +33,7 @@ def test_retrieve_article_by_id(client, user):
4633
)
4734

4835
url = reverse(
49-
"articles:v1:article-detail-by-id-or-slug",
36+
"articles:v1:articles-detail-by-id-or-slug",
5037
kwargs={"identifier": str(article.id)},
5138
)
5239

@@ -68,7 +55,7 @@ def test_retrieve_article_by_slug(client, user):
6855
)
6956

7057
url = reverse(
71-
"articles:v1:article-detail-by-id-or-slug",
58+
"articles:v1:articles-detail-by-id-or-slug",
7259
kwargs={"identifier": article.slug},
7360
)
7461

@@ -93,7 +80,7 @@ def test_staff_can_access_unpublished_article(client):
9380
)
9481

9582
url = reverse(
96-
"articles:v1:article-detail-by-id-or-slug",
83+
"articles:v1:articles-detail-by-id-or-slug",
9784
kwargs={"identifier": str(article.id)},
9885
)
9986

frontends/api/src/generated/v1/api.ts

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openapi/specs/v1.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ paths:
127127
/api/v1/articles/detail/{identifier}/:
128128
get:
129129
operationId: articles_detail_retrieve
130-
description: If the parameter is numeric, retrieve by ID. Otherwise, slug.
130+
description: If the path parameter is numeric → ID, else → slug.
131131
summary: Retrieve article by ID or slug
132132
parameters:
133133
- in: path
@@ -146,7 +146,7 @@ paths:
146146
$ref: '#/components/schemas/RichTextArticle'
147147
description: ''
148148
'404':
149-
description: Not found
149+
description: No response body
150150
/api/v1/content_file_search/:
151151
get:
152152
operationId: content_file_search_retrieve

0 commit comments

Comments
 (0)