@django-stack/cli 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/bin.js +65 -20
  2. package/dist/index.d.ts +24 -9
  3. package/dist/index.js +1 -1
  4. package/dist/{production-M2SsuuBb.js → production-WThC1aS2.js} +267 -30
  5. package/package.json +1 -1
  6. package/templates/backend/Dockerfile +7 -2
  7. package/templates/backend/config/settings.py +13 -0
  8. package/templates/backend/config/urls.py +11 -0
  9. package/templates/backend/content/admin.py +14 -5
  10. package/templates/backend/content/models.py +2 -2
  11. package/templates/backend/content/tests/test_api.py +15 -3
  12. package/templates/backend/pyproject.toml +1 -1
  13. package/templates/base/.github/workflows/deploy.yml +10 -1
  14. package/templates/base/AGENTS.md +86 -8
  15. package/templates/base/Caddyfile +24 -0
  16. package/templates/base/README.md +74 -6
  17. package/templates/base/SECURITY.md +2 -0
  18. package/templates/base/compose.yaml +28 -2
  19. package/templates/base/env.example +37 -0
  20. package/templates/frontend-astro/AGENTS.md +3 -3
  21. package/templates/frontend-astro/Dockerfile +6 -3
  22. package/templates/frontend-astro/astro.config.mjs +16 -5
  23. package/templates/frontend-astro/package.json +1 -1
  24. package/templates/frontend-astro/pnpm-workspace.yaml +17 -0
  25. package/templates/frontend-astro/src/components/ContactForm.astro +19 -3
  26. package/templates/frontend-astro/src/middleware.ts +9 -1
  27. package/templates/frontend-next/AGENTS.md +3 -3
  28. package/templates/frontend-next/Dockerfile +5 -2
  29. package/templates/frontend-next/package.json +3 -3
  30. package/templates/frontend-next/pnpm-workspace.yaml +11 -0
  31. package/templates/frontend-next/src/app/[...path]/page.tsx +2 -1
  32. package/templates/frontend-next/src/components/ContactForm.tsx +14 -3
  33. package/templates/frontend-next/src/instrumentation.ts +12 -0
  34. package/templates/frontend-next/src/proxy.ts +4 -1
  35. package/templates/frontend-nuxt/AGENTS.md +3 -3
  36. package/templates/frontend-nuxt/Dockerfile +5 -2
  37. package/templates/frontend-nuxt/nuxt.config.ts +7 -3
  38. package/templates/frontend-nuxt/package.json +2 -2
  39. package/templates/frontend-nuxt/pnpm-workspace.yaml +13 -0
  40. package/templates/frontend-nuxt/server/plugins/errors.ts +14 -0
  41. package/templates/frontend-nuxt/server/routes/_content/layout.get.ts +4 -0
  42. package/templates/frontend-nuxt/src/components/ContactForm.vue +15 -2
  43. package/templates/frontend-sveltekit/AGENTS.md +3 -3
  44. package/templates/frontend-sveltekit/Dockerfile +6 -3
  45. package/templates/frontend-sveltekit/package.json +2 -2
  46. package/templates/frontend-sveltekit/pnpm-workspace.yaml +11 -0
  47. package/templates/frontend-sveltekit/src/hooks.server.ts +13 -1
  48. package/templates/frontend-sveltekit/src/lib/components/ContactForm.svelte +15 -2
  49. package/templates/frontend-sveltekit/src/routes/+layout.server.ts +3 -0
  50. package/templates/frontend-sveltekit/src/routes/[...path]/+page.svelte +1 -1
  51. package/templates/frontend-sveltekit/vite.config.ts +10 -3
  52. package/templates/frontend-templates/template.json +5 -0
  53. package/templates/frontend-templates/website/__init__.py +0 -0
  54. package/templates/frontend-templates/website/apps.py +6 -0
  55. package/templates/frontend-templates/website/context_processors.py +16 -0
  56. package/templates/frontend-templates/website/forms.py +20 -0
  57. package/templates/frontend-templates/website/static/website/favicon.svg +1 -0
  58. package/templates/frontend-templates/website/static/website/global.css +232 -0
  59. package/templates/frontend-templates/website/templates/website/404.html +10 -0
  60. package/templates/frontend-templates/website/templates/website/base.html +44 -0
  61. package/templates/frontend-templates/website/templates/website/blocks/call_to_action.html +5 -0
  62. package/templates/frontend-templates/website/templates/website/blocks/image.html +4 -0
  63. package/templates/frontend-templates/website/templates/website/blocks/quote.html +4 -0
  64. package/templates/frontend-templates/website/templates/website/blocks/text.html +2 -0
  65. package/templates/frontend-templates/website/templates/website/home.html +8 -0
  66. package/templates/frontend-templates/website/templates/website/includes/contact_form.html +21 -0
  67. package/templates/frontend-templates/website/templates/website/includes/post_card.html +9 -0
  68. package/templates/frontend-templates/website/templates/website/includes/post_list.html +11 -0
  69. package/templates/frontend-templates/website/templates/website/page.html +10 -0
  70. package/templates/frontend-templates/website/templates/website/post.html +20 -0
  71. package/templates/frontend-templates/website/templates/website/post_list.html +11 -0
  72. package/templates/frontend-templates/website/templates/website/robots.txt +5 -0
  73. package/templates/frontend-templates/website/templates/website/rss.xml +16 -0
  74. package/templates/frontend-templates/website/templates/website/sitemap.xml +8 -0
  75. package/templates/frontend-templates/website/tests/__init__.py +0 -0
  76. package/templates/frontend-templates/website/tests/test_site.py +102 -0
  77. package/templates/frontend-templates/website/urls.py +17 -0
  78. package/templates/frontend-templates/website/views.py +176 -0
@@ -0,0 +1,176 @@
1
+ """
2
+ The pages of the site, rendered by Django. They read the content through
3
+ the ORM with ``published()``, which hides drafts and scheduled posts
4
+ unless an editor opened a preview, the same rules as the content API.
5
+
6
+ ``@site_page`` caches each page until content changes (in Django's cache,
7
+ the database by default) and sets the Content-Security-Policy.
8
+ """
9
+
10
+ from django.conf import settings
11
+ from django.core.paginator import Paginator
12
+ from django.http import Http404, HttpResponseNotAllowed
13
+ from django.shortcuts import get_object_or_404, redirect, render
14
+ from django.views.decorators.http import require_safe
15
+ from django_stack.contrib.site import published, site_page
16
+ from rest_framework.throttling import SimpleRateThrottle
17
+
18
+ from content.models import Category, Page, Post, SiteSettings, Tag
19
+
20
+ from .forms import ContactForm
21
+
22
+ POSTS_PER_PAGE = 9
23
+
24
+
25
+ def posts(request):
26
+ return published(Post.objects.select_related("author").prefetch_related("categories", "tags"), request)
27
+
28
+
29
+ def post_list(request, queryset):
30
+ """One page of posts, from the ?page= parameter."""
31
+ number = request.GET.get("page", "1")
32
+ return Paginator(queryset, POSTS_PER_PAGE).get_page(number if number.isdigit() else 1)
33
+
34
+
35
+ @site_page
36
+ @require_safe
37
+ def home(request):
38
+ site = SiteSettings.objects.first()
39
+ home_page = site.home_page if site else None
40
+ if home_page and not published(Page.objects.filter(pk=home_page.pk), request).exists():
41
+ home_page = None
42
+ if home_page:
43
+ return render(request, "website/page.html", {"page": home_page})
44
+ return render(request, "website/home.html", {"posts": post_list(request, posts(request))})
45
+
46
+
47
+ @site_page
48
+ @require_safe
49
+ def blog(request):
50
+ return render(
51
+ request,
52
+ "website/post_list.html",
53
+ {"title": "Blog", "seo_page": {"title": "Blog"}, "posts": post_list(request, posts(request))},
54
+ )
55
+
56
+
57
+ @site_page
58
+ @require_safe
59
+ def post(request, slug):
60
+ item = get_object_or_404(posts(request), slug=slug)
61
+ return render(request, "website/post.html", {"post": item})
62
+
63
+
64
+ @site_page
65
+ @require_safe
66
+ def category(request, slug):
67
+ item = get_object_or_404(Category, slug=slug)
68
+ return render(
69
+ request,
70
+ "website/post_list.html",
71
+ {
72
+ "title": item.name,
73
+ "seo_page": {"title": item.name, "seo_description": item.description},
74
+ "label": "Category",
75
+ "description": item.description,
76
+ "posts": post_list(request, posts(request).filter(categories=item)),
77
+ },
78
+ )
79
+
80
+
81
+ @site_page
82
+ @require_safe
83
+ def tag(request, slug):
84
+ item = get_object_or_404(Tag, slug=slug)
85
+ return render(
86
+ request,
87
+ "website/post_list.html",
88
+ {
89
+ "title": f"#{item.name}",
90
+ "seo_page": {"title": item.name},
91
+ "label": "Tag",
92
+ "posts": post_list(request, posts(request).filter(tags=item)),
93
+ },
94
+ )
95
+
96
+
97
+ @site_page
98
+ def page(request, path):
99
+ item = published(Page.objects.all(), request).filter(path=path.strip("/")).first()
100
+ if item is None:
101
+ raise Http404
102
+ if item.path == "contact":
103
+ return contact(request, item)
104
+ if request.method not in ("GET", "HEAD"):
105
+ return HttpResponseNotAllowed(["GET", "HEAD"])
106
+ return render(request, "website/page.html", {"page": item})
107
+
108
+
109
+ class ContactThrottle(SimpleRateThrottle):
110
+ """The API's limit for public forms, per IP address: PUBLIC_WRITE_THROTTLE_RATE (10/minute)."""
111
+
112
+ scope = "public_write"
113
+
114
+ def get_cache_key(self, request, view):
115
+ return self.cache_format % {"scope": self.scope, "ident": self.get_ident(request)}
116
+
117
+
118
+ @site_page(cache_page=False, captcha=True)
119
+ def contact(request, item):
120
+ """The contact page: its blocks and the form, which posts to itself."""
121
+ form = ContactForm(request.POST or None)
122
+ sent = request.GET.get("sent") == "1"
123
+ status = 200
124
+ if request.method == "POST":
125
+ throttle = ContactThrottle()
126
+ if not throttle.allow_request(request, None):
127
+ form.add_error(None, "Too many messages; try again in a minute.")
128
+ status = 429
129
+ elif form.is_valid():
130
+ form.save()
131
+ # Post/redirect/get: a reload does not send the message again.
132
+ return redirect(f"{request.path}?sent=1")
133
+ else:
134
+ status = 400
135
+ return render(request, "website/page.html", {"page": item, "form": form, "sent": sent}, status=status)
136
+
137
+
138
+ @site_page
139
+ @require_safe
140
+ def rss(request):
141
+ site = SiteSettings.objects.first()
142
+ return render(
143
+ request,
144
+ "website/rss.xml",
145
+ {"site": site, "site_url": settings.SITE_URL, "posts": posts(request).order_by("-published_at")[:20]},
146
+ content_type="application/rss+xml; charset=utf-8",
147
+ )
148
+
149
+
150
+ @site_page
151
+ @require_safe
152
+ def sitemap(request):
153
+ site = SiteSettings.objects.first()
154
+ pages = published(Page.objects.all(), request)
155
+ if site and site.home_page_id:
156
+ pages = pages.exclude(pk=site.home_page_id)
157
+ return render(
158
+ request,
159
+ "website/sitemap.xml",
160
+ {"site_url": settings.SITE_URL, "pages": pages, "posts": posts(request)},
161
+ content_type="application/xml; charset=utf-8",
162
+ )
163
+
164
+
165
+ @site_page
166
+ @require_safe
167
+ def robots(request):
168
+ return render(
169
+ request, "website/robots.txt", {"site_url": settings.SITE_URL}, content_type="text/plain; charset=utf-8"
170
+ )
171
+
172
+
173
+ @site_page(cache_page=False)
174
+ def not_found(request, exception=None):
175
+ """The 404 page, with the site's layout (handler404 in config/urls.py)."""
176
+ return render(request, "website/404.html", {"not_found": {"title": "Page not found"}}, status=404)