| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 |
- from __future__ import annotations
- from django.db import models
- from django.contrib.auth import get_user_model
- from django.utils.text import slugify
- from textwrap import dedent
- from django.utils import timezone
- from taggit.managers import TaggableManager
- from mptt.models import MPTTModel, TreeForeignKey
- from markdownfield.models import MarkdownField, RenderedMarkdownField
- from markdownfield.validators import VALIDATOR_STANDARD
- from orgs.models import Organization
- from typing import Optional
- User = get_user_model()
- class PostCategory(MPTTModel):
- organization = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name="cms_categories", null=True, blank=True)
- name = models.CharField(max_length=120)
- slug = models.SlugField(max_length=140, unique=False, blank=True)
- description = models.TextField(blank=True)
- parent = TreeForeignKey(
- "self", on_delete=models.CASCADE, null=True, blank=True, related_name="children"
- )
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
- class MPTTMeta:
- order_insertion_by = ["name"]
- class Meta:
- ordering = ("tree_id", "lft", "name")
- indexes = [
- models.Index(fields=["slug"]),
- ]
- unique_together = (("organization", "slug"),)
- def save(self, *args, **kwargs):
- if not self.slug:
- self.slug = slugify(self.name)[:140]
- super().save(*args, **kwargs)
- def __str__(self) -> str: # pragma: no cover - trivial
- return self.name
- class Post(models.Model):
- STATUS_DRAFT = "draft"
- STATUS_PUBLISHED = "published"
- STATUS_CHOICES = (
- (STATUS_DRAFT, "Draft"),
- (STATUS_PUBLISHED, "Published"),
- )
- organization = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name="cms_posts", null=True, blank=True)
- title = models.CharField(max_length=200)
- slug = models.SlugField(max_length=220, unique=False, blank=True)
- category = TreeForeignKey(PostCategory, on_delete=models.PROTECT, related_name="posts")
- author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="posts")
- excerpt = models.TextField(blank=True)
- feature_image = models.ImageField(upload_to="posts/feature/%Y/%m/%d/", null=True, blank=True)
- # Markdown content source + auto-rendered HTML
- content = MarkdownField(rendered_field="content_html", validator=VALIDATOR_STANDARD)
- content_html = RenderedMarkdownField(blank=True, null=True)
- status = models.CharField(max_length=12, choices=STATUS_CHOICES, default=STATUS_DRAFT)
- published_at = models.DateTimeField(null=True, blank=True)
- created_at = models.DateTimeField(auto_now_add=True)
- updated_at = models.DateTimeField(auto_now=True)
- # Optional tags, consistent with project usage
- tags = TaggableManager(blank=True)
- class Meta:
- ordering = ("-published_at", "-created_at", "-id")
- indexes = [
- models.Index(fields=["status", "published_at"]),
- models.Index(fields=["slug"]),
- ]
- unique_together = (("organization", "slug"),)
- def save(self, *args, **kwargs):
- if not self.slug and self.title:
- base = slugify(self.title)[:220].strip('-')
- slug = base or "post"
- # Ensure uniqueness within organization by appending -2, -3, ... if needed
- i = 2
- qs = Post.objects
- if self.organization_id:
- qs = qs.filter(organization_id=self.organization_id)
- while qs.filter(slug=slug).exclude(pk=self.pk).exists():
- suffix = f"-{i}"
- slug = (base[: 220 - len(suffix)] + suffix).strip('-') or f"post-{i}"
- i += 1
- self.slug = slug
- if self.status == self.STATUS_PUBLISHED and not self.published_at:
- self.published_at = timezone.now()
- super().save(*args, **kwargs)
- def __str__(self) -> str: # pragma: no cover - trivial
- return self.title
- # --- Demo data helpers -------------------------------------------------------
- def create_demo_data(org: Optional[Organization] = None) -> dict:
- """Create demo categories and posts for CMS.
- Idempotent: safe to call multiple times. Returns a summary counts dict.
- """
- created = {"categories": 0, "posts": 0}
- # Categories
- cat_specs = [
- {"name": "News", "description": "Company news and announcements"},
- {"name": "Guides", "description": "How-to guides and tutorials"},
- {"name": "Updates", "description": "Product updates and release notes"},
- ]
- cats: dict[str, PostCategory] = {}
- for spec in cat_specs:
- slug = slugify(spec["name"])[:140]
- lookup = {"slug": slug}
- if org is not None:
- lookup["organization"] = org
- obj, was_created = PostCategory.objects.get_or_create(
- **lookup,
- defaults={
- "name": spec["name"],
- "description": spec.get("description", ""),
- "organization": org,
- },
- )
- if was_created:
- created["categories"] += 1
- cats[obj.name] = obj
- # Choose an author if available
- try:
- author = User.objects.filter(is_staff=True).order_by("id").first() or User.objects.order_by("id").first()
- except Exception:
- author = None
- # Posts
- posts_specs = [
- {
- "title": "Welcome to Ecoloop",
- "category": cats.get("News"),
- "excerpt": "Introducing our recycling operations platform.",
- "content": "# Welcome to Ecoloop\n\nEcoloop helps factories and recyclers manage pickups, pricing, and a scrap marketplace.\nThis is a demo post seeded for your environment.",
- "status": Post.STATUS_PUBLISHED,
- "tags": ["ecoloop", "news"],
- },
- {
- "title": "Getting Started Guide",
- "category": cats.get("Guides"),
- "excerpt": "How to create materials, customers, and pickups.",
- "content": "# Getting Started\n\n1. Create your organization materials and price list.\n2. Add customers and sites.\n3. Create a pickup and record a weigh ticket.\n4. Generate invoices and payouts.",
- "status": Post.STATUS_PUBLISHED,
- "tags": ["guide", "howto"],
- },
- {
- "title": "September Release Notes",
- "category": cats.get("Updates"),
- "excerpt": "Highlights from the latest release.",
- "content": "# Release Notes\n\n- New scrap listings and bids\n- Auto award expired listings\n- Admin UI for pickups and invoices",
- "status": Post.STATUS_DRAFT,
- "tags": ["updates", "releases"],
- },
- ]
- for spec in posts_specs:
- base_slug = slugify(spec["title"])[:220]
- lookup = {"slug": base_slug}
- if org is not None:
- lookup["organization"] = org
- # Normalize multiline markdown content to avoid code-block wrapping
- raw_content = spec.get("content", "")
- content_md = dedent(raw_content).strip()
- excerpt_text = spec.get("excerpt", "")
- if isinstance(excerpt_text, str):
- excerpt_text = dedent(excerpt_text).strip()
- post, was_created = Post.objects.get_or_create(
- **lookup,
- defaults={
- "title": spec["title"],
- "category": spec.get("category") or next(iter(cats.values())),
- "author": author,
- "excerpt": excerpt_text,
- "content": content_md,
- "status": spec.get("status", Post.STATUS_DRAFT),
- "organization": org,
- },
- )
- if was_created:
- created["posts"] += 1
- try:
- tags = spec.get("tags") or []
- if tags:
- post.tags.set(tags)
- except Exception:
- pass
- return created
|