Slugify URLs to news articles
authorMagnus Hagander <magnus@hagander.net>
Fri, 25 Sep 2020 11:46:25 +0000 (13:46 +0200)
committerMagnus Hagander <magnus@hagander.net>
Sat, 26 Sep 2020 12:42:07 +0000 (14:42 +0200)
pgweb/news/admin.py
pgweb/news/feeds.py
pgweb/news/management/commands/twitter_post.py
pgweb/news/models.py
pgweb/news/struct.py
pgweb/news/views.py
pgweb/urls.py
templates/index.html
templates/news/newsarchive.html
templates/security/security.html

index 4c3c8e10a4ebbd561aea937009a90558a497cfad..7eb54bd051a6a3fd15c0b6afa6d3e34f5b8c2ad0 100644 (file)
@@ -1,5 +1,6 @@
 from django.contrib import admin
 from django import forms
+from django.template.defaultfilters import slugify
 
 from pgweb.util.admin import PgwebAdmin
 from pgweb.core.models import OrganisationEmail
@@ -16,13 +17,16 @@ class NewsArticleAdminForm(forms.ModelForm):
 
 
 class NewsArticleAdmin(PgwebAdmin):
-    list_display = ('title', 'org', 'date', 'modstate', )
+    list_display = ('title', 'org', 'date', 'modstate', 'posturl')
     list_filter = ('modstate', )
     filter_horizontal = ('tags', )
     search_fields = ('content', 'title', )
     exclude = ('modstate', 'firstmoderator', )
     form = NewsArticleAdminForm
 
+    def posturl(self, obj):
+        return '/about/news/{}-{}/'.format(slugify(obj.title), obj.id)
+
 
 class NewsTagAdmin(PgwebAdmin):
     list_display = ('urlname', 'name', 'description')
index 0904d2f640d5710a42ef16c86250b93207f28a5d..d0927dc866597ebc27d603191c97a3d172a032d8 100644 (file)
@@ -1,4 +1,5 @@
 from django.contrib.syndication.views import Feed
+from django.template.defaultfilters import slugify
 
 from pgweb.util.moderation import ModerationState
 from .models import NewsArticle
@@ -23,7 +24,7 @@ class NewsFeed(Feed):
             return NewsArticle.objects.filter(modstate=ModerationState.APPROVED)[:10]
 
     def item_link(self, obj):
-        return "https://www.postgresql.org/about/news/%s/" % obj.id
+        return "https://www.postgresql.org/about/news/{}-{}/".format(slugify(obj.title), obj.id)
 
     def item_pubdate(self, obj):
         return datetime.combine(obj.date, time.min)
index d3a1ba7d8a696b8536595f82a8bd3cd65f065144..6ceb7d7876dda2f044eb31a1179bc2633d1ea37d 100644 (file)
@@ -6,6 +6,7 @@
 
 from django.core.management.base import BaseCommand, CommandError
 from django.db import connection
+from django.template.defaultfilters import slugify
 from django.conf import settings
 
 from datetime import datetime, timedelta
@@ -37,7 +38,7 @@ class Command(BaseCommand):
 
         for a in articles:
             # We hardcode 30 chars for the URL shortener. And then 10 to cover the intro and spacing.
-            statusstr = "News: {0} {1}/about/news/{2}/".format(a.title[:140 - 40], settings.SITE_ROOT, a.id)
+            statusstr = "News: {0} {1}/about/news/{2}-{3}/".format(a.title[:140 - 40], settings.SITE_ROOT, slugify(a.title), a.id)
             r = tw.post('https://api.twitter.com/1.1/statuses/update.json', data={
                 'status': statusstr,
             })
index 95bcef7514c6fbe64729ffbc8169e5b708f2c5a5..d4920ef5599b7252ded00ceec03035953cb21d98 100644 (file)
@@ -43,6 +43,7 @@ class NewsArticle(TwoModeratorsMixin, TristateModerateModel):
 
     def purge_urls(self):
         yield '/about/news/%s/' % self.pk
+        yield '/about/news/.*-%s/' % self.pk
         yield '/about/newsarchive/'
         yield '/news.rss'
         yield '/news/.*.rss'
index 6af63dc3bcd57307e1777a5c33f9de950459c8ff..38a376bcb91c145b60dbc47af521772f7587e88c 100644 (file)
@@ -1,3 +1,5 @@
+from django.template.defaultfilters import slugify
+
 from datetime import date, timedelta
 from .models import NewsArticle
 
@@ -16,5 +18,5 @@ def get_struct():
         yearsold = (now - n.date).days / 365
         if yearsold > 4:
             yearsold = 4
-        yield ('about/news/%s/' % n.id,
+        yield ('about/news/{}-{}/'.format(slugify(n.title), n.id),
                0.5 - (yearsold / 10.0))
index 163d3f2e9debff43fd501103ebc7176c3ec23fc3..d06a8779f88cfa9d3eb448d3dd63bc5af48557f5 100644 (file)
@@ -1,5 +1,6 @@
 from django.shortcuts import get_object_or_404
-from django.http import HttpResponse, Http404
+from django.http import HttpResponse, Http404, HttpResponsePermanentRedirect
+from django.template.defaultfilters import slugify
 
 from pgweb.util.contexts import render_pgweb
 from pgweb.util.moderation import ModerationState
@@ -23,10 +24,12 @@ def archive(request, tag=None, paging=None):
     })
 
 
-def item(request, itemid, throwaway=None):
+def item(request, itemid, slug=None):
     news = get_object_or_404(NewsArticle, pk=itemid)
     if news.modstate != ModerationState.APPROVED:
         raise Http404
+    if slug != slugify(news.title):
+        return HttpResponsePermanentRedirect('/about/news/{}-{}/'.format(slugify(news.title), news.id))
     return render_pgweb(request, 'about', 'news/item.html', {
         'obj': news,
         'newstags': NewsTag.objects.all(),
index c3a364f18dd76e2e5c641bacb10328e099491fb0..2c5d754ce8c1d85981e72520cc0580de59a43174 100644 (file)
@@ -34,7 +34,8 @@ urlpatterns = [
 
     url(r'^about/$', pgweb.core.views.about),
     url(r'^about/newsarchive/([^/]+/)?$', pgweb.news.views.archive),
-    url(r'^about/news/(\d+)(-.*)?/$', pgweb.news.views.item),
+    url(r'^about/news/(?P<itemid>\d+)(?P<slug>-.*)?/$', pgweb.news.views.item),
+    url(r'^about/news/(?P<slug>[^/]+)-(?P<itemid>\d+)/$', pgweb.news.views.item),
     url(r'^about/news/taglist.json/$', pgweb.news.views.taglist_json),
     url(r'^about/events/$', pgweb.events.views.main),
     url(r'^about/eventarchive/$', pgweb.events.views.archive),
index 7522535e835010c9e2a6ce167a0a836b45cc29a5..d693668a9a94a0f18e3feefd376fbb5a741f1d74 100644 (file)
         <ul class="divided">
           {% for n in news %}
             <li>
-              <h3><a href="/about/news/{{ n.id }}/">{{ n.title }}</a></h3>
+              <h3><a href="/about/news/{{ n.title|slugify }}-{{ n.id }}/">{{ n.title }}</a></h3>
               <ul class="meta">
                 <li><i class="far fa-clock"></i>&nbsp;{{ n.displaydate }} by {{ n.org.name }}</li>
               </ul>
index b687c9ddd98f08100b66e15f02386dd489677c62..2324e3abdf84d34edb7362e6217619d96004d607 100644 (file)
@@ -9,10 +9,10 @@
 </p>
 
 {%for obj in news %}
-<h2 class="news"><a href="/about/news/{{obj.id}}/">{{obj.title}}</a></h2>
+<h2 class="news"><a href="/about/news/{{obj.title|slugify}}-{{obj.id}}/">{{obj.title}}</a></h2>
 <div class="newsdate">Posted on <strong>{{obj.displaydate}}</strong>{% if obj.org.name != '_migrated' %} by {{ obj.org.name }}{% endif %}</div>
 {{obj.content|markdown:"safe"|striptags|truncatewords:20}}
-<p><a href="/about/news/{{obj.id}}/">Read more...</a></p>
+<p><a href="/about/news/{{obj.title|slugify}}-{{obj.id}}/">Read more...</a></p>
 {%endfor%}
 <p><a href="/account/news/new/">Submit news</a></p>
 {%endblock%}
index 1466fc3b4615eea8416cd1570b0767bf78fe624c..a6e0a6fca8db15ee5e03a2e484140a23d2bd9550 100644 (file)
@@ -76,7 +76,7 @@ You can filter the view of patches to show just patches for version:<br/>
       <tr>
         <td>
           {%if p.cve%}<nobr>{%if p.cve_visible%}<a href="{{p.cvelink}}">CVE-{{p.cve}}</a>{%else%}CVE-{{p.cve}}{%endif%}</nobr><br/>{%endif%}
-          {%if p.newspost%}<a href="/about/news/{{p.newspost.id}}/">Announcement</a><br/>{%endif%}
+          {%if p.newspost%}<a href="/about/news/{{p.newspost.title|slugify}}-{{p.newspost.id}}/">Announcement</a><br/>{%endif%}
         </td>
         <td>{{p.affected|join:", "}}</td>
         <td>{{p.fixed|join:", "}}</td>