Ross Symonds
Ross Symonds

Reputation: 87

Adding Slugs to URL pattern

[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'),

enter image description here

[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})

enter image description here

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

Answers (2)

Daniel Morell
Daniel Morell

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

&#214;zer
&#214;zer

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

Related Questions