~linuxgoose/bocpress

90ea2d55763ab584316c5af3c2ed9cbbe5921d9b — Jordan Robinson 4 months ago d8759d9
add tags to posts
M main/feeds.py => main/feeds.py +6 -0
@@ 107,6 107,12 @@ class RSSBlogFeed(Feed):
    def item_pubdate(self, item):
        # set time to 00:00 because we don't store time for published_at field
        return datetime.combine(item.published_at, datetime.min.time())
    
    def item_categories(self, item):
        """
        Return a list of tags for this item, to render <category> elements.
        """
        return item.tag_list  # uses your Post.tag_list property
        
    def item_extra_kwargs(self, item):
        """

M main/forms.py => main/forms.py +1 -0
@@ 73,3 73,4 @@ class APIPost(forms.Form):
    slug = forms.SlugField(max_length=300, required=False)
    body = forms.CharField(widget=forms.Textarea, required=False)
    published_at = forms.DateField(required=False)
    tags = forms.CharField(max_length=300, required=False)

A main/migrations/0121_post_tags.py => main/migrations/0121_post_tags.py +18 -0
@@ 0,0 1,18 @@
# Generated by Django 5.2.5 on 2025-09-20 20:52

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('main', '0120_user_number_of_posts_feed'),
    ]

    operations = [
        migrations.AddField(
            model_name='post',
            name='tags',
            field=models.CharField(blank=True, default=None, max_length=300, null=True),
        ),
    ]

M main/models.py => main/models.py +7 -0
@@ 264,6 264,7 @@ class Post(models.Model):
        help_text="Leave blank to keep as draft/unpublished. Use a future date for auto-posting.",
    )
    broadcasted_at = models.DateTimeField(blank=True, null=True, default=None)
    tags = models.CharField(max_length=300, blank=True, null=True, default=None)

    class Meta:
        ordering = ["-published_at", "-created_at"]


@@ 277,6 278,12 @@ class Post(models.Model):
    def body_as_text(self):
        as_html = util.md_to_html(self.body)
        return bleach.clean(as_html, strip=True, tags=[])
    
    @property
    def tag_list(self):
        if self.tags:
            return [t.strip() for t in self.tags.split(",") if t.strip()]
        return []

    @property
    def is_draft(self):

M main/templates/main/api_docs.html => main/templates/main/api_docs.html +2 -0
@@ 54,6 54,7 @@
        <li><code>title</code>: string [required]</li>
        <li><code>body</code>: string [optional]</li>
        <li><code>published_at</code>: string (ISO date eg. 2006-01-31) [optional]</li>
        <li><code>tags</code>: string [optional]</li>
    </ul>

    <strong>Request</strong>


@@ 61,6 62,7 @@
    "title": "New blog",
    "body": "## Why?\n\nEveryone wants a blog, right?",
    "published_at": "2020-09-21"
    "tags": "life, tech, coding",
}</code></pre>

    <strong>Response</strong>

M main/templates/main/blog_posts.html => main/templates/main/blog_posts.html +16 -0
@@ 43,6 43,12 @@

    <h2 itemprop="name">{% if blog_user.posts_page_title %}{{ blog_user.posts_page_title }}{% else %}Posts{% endif %}</h2>

    {% if filter_tag %}
    <p>
        Active tag filter: <strong>{{ filter_tag }}</strong> (<a href="{% url 'post_list' %}">clear filter</a>)
    </p>
    {% endif %}

    {% if request.user.is_authenticated and request.subdomain == request.user.username and drafts %}
    <div class="drafts">
        <strong>


@@ 63,8 69,18 @@
        {% if p.published_at %}
        <li>
            <a href="{% url 'post_detail' p.slug %}">{{ p.title }}</a>
            {% if p.tag_list %}
            <small>
                <b>Tags</b>:
                {% for tag in p.tag_list %}
                    <a href="{% url 'post_list_filter' tag %}" class="tag">{{ tag }}</a>{% if not forloop.last %}, {% endif %}
                {% endfor %}
            </small>
            {% endif %}
            <small>
                <b>Published on</b>:
                <time datetime="{{ p.published_at|date:'Y-m-d' }}" itemprop="datePublished">
                    {{ p.published_at|date:'F j, Y' }}
                </time>

M main/templates/main/post_detail.html => main/templates/main/post_detail.html +9 -0
@@ 63,6 63,15 @@
        <div class="posts-item-body" itemprop="articleBody">
            {{ post.body_as_html|safe }}
        </div>

        {% if post.tag_list %}
        <hr>
        <p><b>Tags</b>:
        {% for tag in post.tag_list %}
            <a href="{% url 'post_list_filter' tag %}" class="tag">{{ tag }}</a>{% if not forloop.last %}, {% endif %}
        {% endfor %}
        </p>
        {% endif %}
    </article>
</main>


M main/templates/main/post_form.html => main/templates/main/post_form.html +11 -0
@@ 62,6 62,17 @@
        </p>

        <p>
            <label for="id_tags">Tags</label>
            {% if form.tags.errors %}
                {% for error in form.tags.errors %}
                    <span class="form-error">{{ error|escape }}</span><br>
                {% endfor %}
            {% endif %}
            <input type="text" name="tags" id="id_tags" maxlength="300" required value="{{ form.tags.value|default_if_none:'' }}">
            <span class="helptext">{{ form.tags.help_text }}</span>
        </p>

        <p>
            <label for="id_body">Content (<a href="{% url 'guides_markdown' %}">supports markdown</a>) <span id="js-status"></span></label>
            {% if form.body.errors %}
                {% for error in form.body.errors %}

M main/templates/main/post_list.html => main/templates/main/post_list.html +16 -0
@@ 9,6 9,11 @@
        <a href="{% url 'post_create' %}">Create a new post »</a>
    </p>
    {% if post_list %}
    {% if filter_tag %}
    <p>
        Active tag filter: <strong>{{ filter_tag }}</strong> (<a href="{% url 'post_list' %}">clear filter</a>)
    </p>
    {% endif %}
    <p>
        List of posts:
    </p>


@@ 18,7 23,18 @@
            <a href="{% url 'post_detail' post.slug %}">
                {{ post.title }}
            </a>
            {% if p.tag_list %}
            <small>
                <b>Tags</b>:
                {% for tag in p.tag_list %}
                    <a href="{% url 'post_list_filter' tag %}" class="tag">{{ tag }}</a>{% if not forloop.last %}, {% endif %}
                {% endfor %}
            </small>
            {% endif %}
            <small>
                <b>Published on</b>:
                {% if post.is_published %}
                <time datetime="{{ post.published_at|date:'Y-m-d' }}" itemprop="datePublished">
                    — {{ post.published_at|date:'F j, Y' }}

M main/urls.py => main/urls.py +1 -0
@@ 92,6 92,7 @@ urlpatterns += [
    ),
    path("new/post/", general.PostCreate.as_view(), name="post_create"),
    path("posts/", general.post_list, name="post_list"),
    path("posts/tag/<slug:tag>/", general.post_list_filter, name="post_list_filter"),
    path("blog/<slug:slug>/", general.PostDetail.as_view(), name="post_detail"),
    path("posts/<slug:slug>/", general.post_detail_redir, name="post_detail_redir_a"),
    path("post/<slug:slug>/", general.post_detail_redir, name="post_detail_redir_b"),

M main/views/general.py => main/views/general.py +63 -2
@@ 5,6 5,7 @@ from datetime import datetime, timedelta
from urllib.parse import urlparse

import stripe
from django.db.models import Q
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import authenticate, login


@@ 167,6 168,66 @@ def post_list(request):
            return redirect("post_list_dashboard")
        
        return render(request, "404.html")

def post_list_filter(request, tag = None):
    if hasattr(request, "subdomain"):
        if models.User.objects.filter(username=request.subdomain).exists():
            drafts = []
            if request.user.is_authenticated and request.user == request.blog_user:
                posts = models.Post.objects.filter(Q(tags__iexact=tag) | 
                                                   Q(tags__startswith=f"{tag},") | 
                                                   Q(tags__endswith=f",{tag}") | 
                                                   Q(tags__contains=f",{tag},"),owner=request.blog_user).defer(
                    "body",
                )
                drafts = models.Post.objects.filter(
                    owner=request.blog_user,
                    published_at__isnull=True,
                    tags__icontains=tag,
                ).defer("body")
            else:
                models.AnalyticPage.objects.create(user=request.blog_user, path="index")
                posts = models.Post.objects.filter(
                    Q(tags__iexact=tag) | 
                    Q(tags__startswith=f"{tag},") | 
                    Q(tags__endswith=f",{tag}") | 
                    Q(tags__contains=f",{tag},"),
                    owner=request.blog_user,
                    published_at__isnull=False,
                    published_at__lte=timezone.now().date(),
                ).defer("body")

            license_url = request.build_absolute_uri(reverse('rsl_license'))

            response = render(
                request,
                "main/blog_posts.html",
                {
                    "subdomain": request.subdomain,
                    "blog_user": request.blog_user,
                    "posts": posts,
                    "drafts": drafts,
                    "pages": models.Page.objects.filter(
                        owner=request.blog_user, is_hidden=False
                    ).defer("body"),
                    "license_url": license_url,
                    "filter_tag": tag,
                },
            )
        
            # add Link header for license if applicable
            obj, created = models.ReallySimpleLicensing.objects.get_or_create(user=request.blog_user)
            if request.blog_user.reallysimplelicensing.license and request.blog_user.reallysimplelicensing.show_http:
                response["Link"] = f'<{license_url}>; rel="license"; type="application/rsl+xml"'

            return response
        else:
            return redirect("//" + settings.CANONICAL_HOST + reverse("index"))
    else:
        if request.user.is_authenticated:
            return redirect("post_list_dashboard")
        
        return render(request, "404.html")
    
class PostList(LoginRequiredMixin, ListView):
    model = models.Post


@@ 423,7 484,7 @@ class PostDetail(DetailView):

class PostCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView):
    model = models.Post
    fields = ["title", "published_at", "body"]
    fields = ["title", "published_at", "body", "tags"]
    success_message = "'%(title)s' was created"

    def form_valid(self, form):


@@ 443,7 504,7 @@ class PostCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView):

class PostUpdate(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
    model = models.Post
    fields = ["title", "slug", "published_at", "body"]
    fields = ["title", "slug", "published_at", "body", "tags"]
    success_message = "post updated"

    def get_queryset(self):