Reputation: 87
[Original Post]
I want users to access posts using either the PK or Slug value. I can get http://localhost:8000/post/8/ to work but not http://localhost:8000/post/working-in-malaysia/.
I have looked at a few posts on Stack Overflow but I don't want http://localhost:8000/post/8/working-in-malaysia. And I don't want it to be a case of either the PK works. Or the slug works. I want the user to be able to enter either.
Below you can see my failed attempts.
urls.py
path('post/<int:pk>/', views.post_detail, name='post-detail'),
#path('post/<slug:the_slug>/', views.post_detail, name='post-detail-slug'),
#path('post/<slug:slug>/', views.post_detail, name='post-detail-slug'),
#path('post/(?P<slug>[-\w]+)/$', views.post_detail, name='post-detail-slug'),
path('<slug:slug>', views.post_detail, name='post-detail-slug'),
[Updates After Looking At Daniel Morell Comments]
views.py
class PostDetailView(DetailView):
model = Post
def dispatch(pk_slug):
if pk_slug.isdigit():
post = Post.objects.filter(id=pk_slug).first()
else:
post = Post.objects.filter(slug=pk_slug).first()
#post = get_object_or_404(Post)
comments = post.comments.filter(active=True, slug=slug)
new_comment = None
if request.method == 'POST':
comment_form = CommentForm(data=request.POST)
if comment_form.is_valid():
new_comment = comment_form.save(commit=False)
new_comment.post = post
new_comment.save()
else:
comment_form = CommentForm()
return render(request, post_detail.html, {'post': post,
'comments': comments,
'new_comment': new_comment,
'comment_form': comment_form})
urls.py
path('', PostListView.as_view(), name='blog-home'),
path('post/<slug:pk_slug>', views.post_detail, name='post-detail'),
path('post/new/', PostCreateView.as_view(), name='post-create'),
models.py
def get_absolute_url(self):
#return reverse('article_detail', kwargs={'slug': self.slug})
return reverse('post-detail', kwargs={'pk': self.pk})
[Updates After Looking At Daniel Morell 2nd Lot Of Comments]
views.py
pk_slug = self.kwargs.get(self.slug_url_kwarg)
# If the pk_slug is not None and it is just digits treat as pk
# Otherwise if it is not None treat as slug
if pk_slug is not None and pk_slug.isdigit():
queryset = queryset.filter(pk=pk_slug)
else pk_slug is not None:
slug_field = self.get_slug_field()
queryset = queryset.filter(**{slug_field: pk_slug})
Command Prompt
File "C:\Users\HP\django_project3\blog\views.py", line 55
else pk_slug is not None:
^
SyntaxError: invalid syntax
[Problem Solved]
For unknown reasons I had a FBV (function based view) and CBV (classed based view) with similar code. Daniel Morell original suggestion worked on the assumption that I was using a FBV.
I unfortunately wasted his time by showing him my code which had the CBV in it. Which meant he re wrote his solution. Then I discovered that the FBV code was redundant and CBV code was correct.
Below is my final set of code. This link is to Daniel Morell original suggestion - https://stackoverflow.com/revisions/61471979/1.
I am a beginner with Python and did not even know what the abbreviation CBV meant yesterday.
views.py
def post_detail(request, pk_slug):
template_name = 'post_detail.html'
if pk_slug.isdigit():
post = Post.objects.filter(id=pk_slug).first()
else:
post = Post.objects.filter(url=pk_slug).first()
comments = Comment.objects.filter(post=post.id ,active=True)
#post = Post.objects.get(pk=pk)
new_comment = None
# Comment posted
if request.method == 'POST':
comment_form = CommentForm(data=request.POST)
if comment_form.is_valid():
# Create Comment object but don't save to database yet
new_comment = comment_form.save(commit=False)
# Assign the current post to the comment
new_comment.post = post
# Save the comment to the database
new_comment.save()
else:
comment_form = CommentForm()
return render(request, template_name, {'post': post,
'comments': comments,
'new_comment': new_comment,
'comment_form': comment_form})
urls.py
path('post/<slug:pk_slug>/', views.post_detail, name='post-detail'),
Upvotes: 1
Views: 1796
Reputation: 2586
There is a trick to simplifying your URL configuration. <slug:__>
will match any string of ASCII letters, numbers, hyphens or underscores. This means you can use slug
to match your primary key.
This is a little different. So you should add a comment to your urlpatterns
explaining that it matches the pk
and the slug
.
Here is an example.
# urls.py
urlpatterns = [
# This matches both the primary key and the slug.
path('post/<slug:pk_slug>/', views.PostDetailView.as_view(), name='post-detail')
]
# views.py
from django.db.models import Q
class PostDetailView(DetailView):
model = Post
slug_url_kwarg = 'pk_slug'
def get_object(self, queryset=None):
# This function overrides DetialView.get_object()
# Use a custom queryset if provided; this is required for subclasses
if queryset is None:
queryset = self.get_queryset()
# Next, look up our primary key or slug
pk_slug = self.kwargs.get(self.slug_url_kwarg)
# If the pk_slug is not None and it is just digits treat as pk
if pk_slug is not None and pk_slug.isdigit():
queryset = queryset.filter(pk=pk_slug)
# Otherwise if it is not None treat as slug
elif pk_slug is not None:
slug_field = self.get_slug_field()
queryset = queryset.filter(**{slug_field: pk_slug})
# Raise an error if there is no pk_slug in the URLconf
if pk_slug is None:
raise AttributeError(
"Generic detail view %s must be called with an object "
"pk_slug in the URLconf." % self.__class__.__name__
)
try:
# Get the single item from the filtered queryset
obj = queryset.get()
except queryset.model.DoesNotExist:
raise Http404(_("No %(verbose_name)s found matching the query") %
{'verbose_name': queryset.model._meta.verbose_name})
return obj
The isdiget()
method returns true if each character in the string is a digit and the string has at least one character.
Thus the following URLs will all be used to match the primary key...
/post/123/
/post/4/
/post/80/
And the following will all be interpreted as slugs...
/post/my-first-post/
/post/23-a-post-slug/
/post/123b/
Note: this will not work with a slug that is made up of all digits.
You should add a validation rule to your post form to make sure the slug has a non-digit character in it.
Upvotes: 1
Reputation: 2106
You can achieve that using djangos re_path
method and using regular expressions. Docs:
https://docs.djangoproject.com/en/3.0/topics/http/urls/#nested-arguments https://docs.djangoproject.com/en/3.0/ref/urls/#re-path
It would be something like this, although this won't work, just to give you an idea:
re_path(r'^post/(?P<pk:int>\d+)|(?P<slug:slug>\w+)/$', ...)
You can try the regex with tools like https://regexr.com/. Just create some of the urls you would expect and use the platform to find the right regex pattern. I haven't tried this myself, so can't tell you exactly how it's done.
Upvotes: 1