Blog tags: add tags to a post, creating the new ones

Python · Django · intermediate · modification

Adds the tagging endpoint for posts. The body is validated first (list size, printable text, slug length), then one `transaction.atomic()` block locks the post row, checks authorship, finds or creates each tag by slug with `get_or_create`, links them with `post.tags.add()` and returns the post's tags. Slugs are processed in sorted order. A `Tag` model with a unique constraint on `slug` and the `Post.tags` many-to-many field come with their migration, and the blog URLs are included under `/api/`. I ran the view against PostgreSQL with concurrent requests, including requests that list the same new tags in opposite orders.

Django 5.1 on PostgreSQL 16, `USE_TZ = True`, session auth (the frontend sends the CSRF token in `X-CSRFToken`), `DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"`, default `READ COMMITTED` isolation. The `blog` app already exists with `Post` and its `0001_initial` migration, which the new migration builds on; `config/urls.py` is the project's root URLconf. Tags are only created by this endpoint (the admin does not register `Tag`), and nothing else writes to the `Tag` table. Post authors are the only users who tag posts, and a post's author never changes. Request bodies are capped at Django's default `DATA_UPLOAD_MAX_MEMORY_SIZE` of 2.5 MB. Error reporting does not capture frame locals.

Requirements

Files touched

--- config/urls.py
 from django.contrib import admin
-from django.urls import path
+from django.urls import include, path
 
 urlpatterns = [
     path("admin/", admin.site.urls),
+    path("api/", include("blog.urls")),
 ]
 
--- blog/models.py
 from django.conf import settings
 from django.db import models
+
+
+class Tag(models.Model):
+    name = models.CharField(max_length=50)
+    slug = models.CharField(max_length=50)
+
+    class Meta:
+        constraints = [
+            models.UniqueConstraint(fields=["slug"], name="unique_tag_slug"),
+        ]
+
+    def __str__(self):
+        return self.name
 
 
     body = models.TextField()
     created_at = models.DateTimeField(auto_now_add=True)

Review this PR

Python practice