M main/admin.py => main/admin.py +1 -0
@@ 60,6 60,7 @@ class UserAdmin(DjUserAdmin):
"post_backups_on",
"show_posts_on_homepage",
"show_posts_in_nav",
+ "show_tags_in_post_list",
"noindex_on",
"reading_time_on",
"export_unsubscribe_key",
A main/migrations/0122_user_show_tags_in_post_list_alter_post_tags.py => main/migrations/0122_user_show_tags_in_post_list_alter_post_tags.py +23 -0
@@ 0,0 1,23 @@
+# Generated by Django 5.2.5 on 2025-09-24 20:42
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('main', '0121_post_tags'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='show_tags_in_post_list',
+ field=models.BooleanField(default=True, help_text='Show/hide tags in the post list.', verbose_name='Show Tags In Post List'),
+ ),
+ migrations.AlterField(
+ model_name='post',
+ name='tags',
+ field=models.CharField(blank=True, default=None, help_text='Enter comma-separated tags (e.g., django, python, blog).', max_length=300, null=True),
+ ),
+ ]
M main/models.py => main/models.py +34 -0
@@ 2,6 2,8 @@ import base64
import binascii
import os
import uuid
+import re
+from django.db.models import Q
import bleach
from django.conf import settings
@@ 17,6 19,31 @@ def _generate_key():
"""Return 32-char random string."""
return binascii.b2a_hex(os.urandom(16)).decode("utf-8")
+# custom queryset and manager for Post model to handle tag operations
+
+class PostQuerySet(models.QuerySet):
+ def with_tag(self, tag):
+ return self.filter(
+ Q(tags__regex=rf'(^|,){re.escape(tag)}(,|$)')
+ )
+
+ def all_unique_tags(self):
+ tags = set()
+ for post in self.exclude(tags__isnull=True).exclude(tags=""):
+ for tag in post.tag_list:
+ tags.add(tag)
+ return sorted(tags)
+
+class PostManager(models.Manager):
+ def get_queryset(self):
+ return PostQuerySet(self.model, using=self._db)
+
+ def with_tag(self, tag):
+ return self.get_queryset().with_tag(tag)
+
+ def all_unique_tags(self):
+ return self.get_queryset().all_unique_tags()
+
class User(AbstractUser):
username = models.CharField(
@@ 117,6 144,11 @@ class User(AbstractUser):
help_text="Show/hide posts in the navigation bar.",
verbose_name="Show Posts In Nav",
)
+ show_tags_in_post_list = models.BooleanField(
+ default=True,
+ help_text="Show/hide tags in the post list.",
+ verbose_name="Show Tags In Post List",
+ )
noindex_on = models.BooleanField(
default=False,
help_text="Add a noindex meta tag so your blog is not indexed by search engines.",
@@ 270,6 302,8 @@ class Post(models.Model):
ordering = ["-published_at", "-created_at"]
unique_together = [["slug", "owner"]]
+ objects = PostManager()
+
@property
def body_as_html(self):
return util.md_to_html(self.body)
M main/templates/main/blog_posts.html => main/templates/main/blog_posts.html +10 -1
@@ 69,7 69,7 @@
{% if p.published_at %}
<li>
<a href="{% url 'post_detail' p.slug %}">{{ p.title }}</a>
- {% if p.tag_list %}
+ {% if p.tag_list and request.user.show_tags_in_post_list %}
<small>
—
<b>Tags</b>:
@@ 91,6 91,15 @@
</li>
{% endif %}
{% endfor %}
+
+ {% if all_tags and request.user.show_tags_in_post_list %}
+ <hr>
+ <p><strong>All tags:</strong>
+ {% for tag in all_tags %}
+ <a href="{% url 'post_list_filter' tag %}" class="tag{% if tag == tag %} active{% endif %}">{{ tag }}</a>{% if not forloop.last %}, {% endif %}
+ {% endfor %}
+ </p>
+ {% endif %}
</ul>
</main>
M main/templates/main/post_list.html => main/templates/main/post_list.html +11 -1
@@ 14,6 14,7 @@
Active tag filter: <strong>{{ filter_tag }}</strong> (<a href="{% url 'post_list' %}">clear filter</a>)
</p>
{% endif %}
+
<p>
List of posts:
</p>
@@ 23,7 24,7 @@
<a href="{% url 'post_detail' post.slug %}">
{{ post.title }}
</a>
- {% if p.tag_list %}
+ {% if p.tag_list and request.user.show_tags_in_post_list %}
<small>
—
<b>Tags</b>:
@@ 48,5 49,14 @@
{% endfor %}
</ul>
{% endif %}
+
+ {% if all_tags and request.user.show_tags_in_post_list %}
+ <hr>
+ <p><strong>All tags:</strong>
+ {% for tag in all_tags %}
+ <a href="{% url 'post_list_dashboard_filter' tag %}" class="tag{% if filter_tag == tag %} active{% endif %}">{{ tag }}</a>{% if not forloop.last %}, {% endif %}
+ {% endfor %}
+ </p>
+ {% endif %}
</main>
{% endblock content %}
M main/urls.py => main/urls.py +1 -0
@@ 80,6 80,7 @@ urlpatterns += [
# blog posts and post snapshots
urlpatterns += [
path("posts-workshop/", general.PostList.as_view(), name="post_list_dashboard"),
+ path("posts-workshop/tag/<slug:tag>/", general.PostList.as_view(), name="post_list_dashboard_filter"),
path(
"post-backups/create/", general.SnapshotCreate.as_view(), name="snapshot_create"
),
M main/views/general.py => main/views/general.py +20 -2
@@ 152,7 152,8 @@ def post_list(request):
"pages": models.Page.objects.filter(
owner=request.blog_user, is_hidden=False
).defer("body"),
- "license_url": license_url
+ "license_url": license_url,
+ "all_tags": models.Post.objects.all_unique_tags()
},
)
@@ 207,6 208,7 @@ def post_list_filter(request, tag = None):
).defer("body"),
"license_url": license_url,
"filter_tag": tag,
+ "all_tags": models.Post.objects.all_unique_tags()
},
)
@@ 228,7 230,22 @@ class PostList(LoginRequiredMixin, ListView):
model = models.Post
def get_queryset(self):
- return models.Post.objects.filter(owner=self.request.user)
+ qs = models.Post.objects.filter(owner=self.request.user)
+
+ #tag = self.request.GET.get("tag")
+ tag = self.kwargs.get("tag")
+ if tag:
+ qs = qs.with_tag(tag)
+
+ return qs
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["filter_tag"] = self.kwargs.get("tag")
+ context["all_tags"] = models.Post.objects.filter(
+ owner=self.request.user
+ ).all_unique_tags()
+ return context
def domain_check(request):
"""
@@ 335,6 352,7 @@ class UserUpdate(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
"post_backups_on",
"show_posts_on_homepage",
"show_posts_in_nav",
+ "show_tags_in_post_list",
"noindex_on",
"reading_time_on",
"robots_txt",