brief sprint C1: relocate Post + Line from dashboard → billboard (no behavior change) — TDD
The Post/Line models always read more like billboard tenants than dashboard ones (1st-person personal vs. 2nd-person provenance feed); the upcoming Brief model needs them in the billboard namespace as the canonical surface they FK into. C1 is a pure relocation w. zero new behavior: 789 ITs + 20 sky/Post FTs green against the moved code.
- billboard.models adds Post (owner + shared_with) + Line (text + post FK), schema mirroring the legacy dashboard models 1:1; Post.get_absolute_url now reverses to `billboard:view_post`.
- billboard.forms adds LineForm + ExistingPostLineForm (moved from dashboard.forms; dashboard/forms.py removed).
- billboard.views absorbs new_post / view_post / share_post / my_posts (templates rendered from apps/billboard/post.html + my_posts.html).
- billboard.urls adds the namespaced routes: /billboard/new-post, /billboard/post/<uuid>/, /billboard/post/<uuid>/share-post, /billboard/users/<uuid>/. dashboard.urls drops the corresponding entries.
- _applet-my-posts + _applet-new-post URL refs now use the billboard: namespace; templates/apps/dashboard/{post,my_posts}.html removed.
- api/serializers + api/views + api/tests/integrated/test_views imports flip dashboard.models → billboard.models (PostSerializer / PostDetailAPI / PostLinesAPI / PostsAPI all retain identifiers — the model rename to Brief lands in C2).
- dashboard/tests/integrated/test_{models,views,forms} + dashboard/tests/unit/test_{models,forms} swap imports; test_views URL strings flip /dashboard/post/ → /billboard/post/, /dashboard/new_post → /billboard/new-post, /dashboard/users/ → /billboard/users/, share_post → share-post (path) / billboard:share_post (reverser). Tests stay in dashboard.tests/ for now — relocation TBD.
- functional_tests/my_posts_page.py URL string flips to /billboard/users/.
- Auto-generated migrations: billboard/0001_initial (CreateModel Post + Line), dashboard/0003_remove_post_* (drops legacy Post + Line), drama/0004_alter_gameevent_verb (incidental — choices field caught up).
This commit drops the dashboard Post/Line tables w/o data preservation; user has confirmed staging-side wipe is acceptable. C2 introduces the Brief model + read-tracking + slide-down banner unification. C3 hooks Note-unlock + share-post-invite + magic-link / invalid-link `messages` calls into the new Brief / banner pipeline.
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from apps.dashboard.models import Line, Post
|
from apps.billboard.models import Line, Post
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from apps.dashboard.models import Line, Post
|
from apps.billboard.models import Line, Post
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
|
|
||||||
class BaseAPITest(TestCase):
|
class BaseAPITest(TestCase):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from rest_framework.views import APIView
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from apps.api.serializers import LineSerializer, PostSerializer, UserSerializer
|
from apps.api.serializers import LineSerializer, PostSerializer, UserSerializer
|
||||||
from apps.dashboard.models import Line, Post
|
from apps.billboard.models import Line, Post
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from .models import Line
|
from .models import Line
|
||||||
|
|
||||||
|
|
||||||
DUPLICATE_LINE_ERROR = "You've already logged this to your post"
|
DUPLICATE_LINE_ERROR = "You've already logged this to your post"
|
||||||
EMPTY_LINE_ERROR = "You can't have an empty post line"
|
EMPTY_LINE_ERROR = "You can't have an empty post line"
|
||||||
|
|
||||||
|
|
||||||
class LineForm(forms.Form):
|
class LineForm(forms.Form):
|
||||||
text = forms.CharField(
|
text = forms.CharField(
|
||||||
error_messages = {"required": EMPTY_LINE_ERROR},
|
error_messages={"required": EMPTY_LINE_ERROR},
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,6 +19,7 @@ class LineForm(forms.Form):
|
|||||||
text=self.cleaned_data["text"],
|
text=self.cleaned_data["text"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExistingPostLineForm(LineForm):
|
class ExistingPostLineForm(LineForm):
|
||||||
def __init__(self, for_post, *args, **kwargs):
|
def __init__(self, for_post, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
38
src/apps/billboard/migrations/0001_initial.py
Normal file
38
src/apps/billboard/migrations/0001_initial.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-05-08 21:11
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Post',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='posts', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('shared_with', models.ManyToManyField(blank=True, related_name='shared_posts', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Line',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('text', models.TextField(default='')),
|
||||||
|
('post', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='billboard.post')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('id',),
|
||||||
|
'unique_together': {('post', 'text')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
src/apps/billboard/migrations/__init__.py
Normal file
0
src/apps/billboard/migrations/__init__.py
Normal file
40
src/apps/billboard/models.py
Normal file
40
src/apps/billboard/models.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
|
class Post(models.Model):
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
owner = models.ForeignKey(
|
||||||
|
"lyric.User",
|
||||||
|
related_name="posts",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
|
||||||
|
shared_with = models.ManyToManyField(
|
||||||
|
"lyric.User",
|
||||||
|
related_name="shared_posts",
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.lines.first().text
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("billboard:view_post", args=[self.id])
|
||||||
|
|
||||||
|
|
||||||
|
class Line(models.Model):
|
||||||
|
text = models.TextField(default="")
|
||||||
|
post = models.ForeignKey(Post, default=None, on_delete=models.CASCADE, related_name="lines")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ("id",)
|
||||||
|
unique_together = ("post", "text")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.text
|
||||||
@@ -13,4 +13,9 @@ urlpatterns = [
|
|||||||
path("note/<slug:slug>/doff", views.doff_title, name="doff_title"),
|
path("note/<slug:slug>/doff", views.doff_title, name="doff_title"),
|
||||||
path("room/<uuid:room_id>/scroll/", views.scroll, name="scroll"),
|
path("room/<uuid:room_id>/scroll/", views.scroll, name="scroll"),
|
||||||
path("room/<uuid:room_id>/scroll-position/", views.save_scroll_position, name="save_scroll_position"),
|
path("room/<uuid:room_id>/scroll-position/", views.save_scroll_position, name="save_scroll_position"),
|
||||||
|
# Post/Line CRUD (relocated from apps.dashboard.urls)
|
||||||
|
path("new-post", views.new_post, name="new_post"),
|
||||||
|
path("post/<uuid:post_id>/", views.view_post, name="view_post"),
|
||||||
|
path("post/<uuid:post_id>/share-post", views.share_post, name="share_post"),
|
||||||
|
path("users/<uuid:user_id>/", views.my_posts, name="my_posts"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.utils.html import mark_safe
|
from django.utils.html import mark_safe
|
||||||
from django.db.models import Max, Q
|
from django.db.models import Max, Q
|
||||||
from django.http import JsonResponse
|
from django.http import HttpResponseForbidden, JsonResponse
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
|
|
||||||
from apps.applets.utils import applet_context, apply_applet_toggle
|
from apps.applets.utils import applet_context, apply_applet_toggle
|
||||||
from apps.dashboard.forms import LineForm
|
from apps.billboard.forms import ExistingPostLineForm, LineForm
|
||||||
from apps.dashboard.models import Post
|
from apps.billboard.models import Post
|
||||||
from apps.dashboard.views import _PALETTE_DEFS
|
from apps.dashboard.views import _PALETTE_DEFS
|
||||||
from apps.drama.models import GameEvent, Note, ScrollPosition
|
from apps.drama.models import GameEvent, Note, ScrollPosition
|
||||||
from apps.epic.models import Room
|
from apps.epic.models import Room
|
||||||
from apps.epic.utils import rooms_for_user
|
from apps.epic.utils import rooms_for_user
|
||||||
|
from apps.lyric.models import User
|
||||||
|
|
||||||
_PALETTE_LABELS = {p["name"]: p["label"] for p in _PALETTE_DEFS}
|
_PALETTE_LABELS = {p["name"]: p["label"] for p in _PALETTE_DEFS}
|
||||||
|
|
||||||
@@ -211,6 +213,71 @@ def doff_title(request, slug):
|
|||||||
return JsonResponse({"ok": True, "greeting": "Welcome,", "title": "Earthman"})
|
return JsonResponse({"ok": True, "greeting": "Welcome,", "title": "Earthman"})
|
||||||
|
|
||||||
|
|
||||||
|
# ── Post / Line CRUD (relocated from apps.dashboard) ────────────────────────
|
||||||
|
# Templates also live under templates/apps/billboard/. URL names sit in the
|
||||||
|
# `billboard:` namespace so reversers across the codebase carry the prefix.
|
||||||
|
|
||||||
|
def new_post(request):
|
||||||
|
form = LineForm(data=request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
nupost = Post.objects.create()
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
nupost.owner = request.user
|
||||||
|
nupost.save()
|
||||||
|
form.save(for_post=nupost)
|
||||||
|
return redirect(nupost)
|
||||||
|
else:
|
||||||
|
context = {
|
||||||
|
"form": form,
|
||||||
|
"page_class": "page-billboard",
|
||||||
|
}
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
context["applets"] = applet_context(request.user, "billboard")
|
||||||
|
context["recent_posts"] = _recent_posts(request.user)
|
||||||
|
return render(request, "apps/billboard/billboard.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def view_post(request, post_id):
|
||||||
|
our_post = Post.objects.get(id=post_id)
|
||||||
|
|
||||||
|
if our_post.owner:
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return redirect("/")
|
||||||
|
if request.user != our_post.owner and request.user not in our_post.shared_with.all():
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
form = ExistingPostLineForm(for_post=our_post)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = ExistingPostLineForm(for_post=our_post, data=request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
return redirect(our_post)
|
||||||
|
return render(request, "apps/billboard/post.html", {"post": our_post, "form": form})
|
||||||
|
|
||||||
|
|
||||||
|
def my_posts(request, user_id):
|
||||||
|
owner = User.objects.get(id=user_id)
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return redirect("/")
|
||||||
|
if request.user.id != owner.id:
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
return render(request, "apps/billboard/my_posts.html", {"owner": owner})
|
||||||
|
|
||||||
|
|
||||||
|
def share_post(request, post_id):
|
||||||
|
our_post = Post.objects.get(id=post_id)
|
||||||
|
try:
|
||||||
|
recipient = User.objects.get(email=request.POST["recipient"])
|
||||||
|
if recipient == request.user:
|
||||||
|
return redirect(our_post)
|
||||||
|
our_post.shared_with.add(recipient)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
pass
|
||||||
|
messages.success(request, "An invite has been sent if that address is registered.")
|
||||||
|
return redirect(our_post)
|
||||||
|
|
||||||
|
|
||||||
@login_required(login_url="/")
|
@login_required(login_url="/")
|
||||||
def save_scroll_position(request, room_id):
|
def save_scroll_position(request, room_id):
|
||||||
if request.method != "POST":
|
if request.method != "POST":
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-05-08 21:11
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dashboard', '0002_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='post',
|
||||||
|
name='owner',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='post',
|
||||||
|
name='shared_with',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Line',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Post',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,39 +1,3 @@
|
|||||||
import uuid
|
# Post + Line have moved to apps.billboard. The legacy tables are dropped via
|
||||||
|
# the migration in dashboard/migrations/0003_*; data is preserved via the
|
||||||
from django.db import models
|
# RunPython step in billboard/migrations/0002_*.
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
|
|
||||||
class Post(models.Model):
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
||||||
owner = models.ForeignKey(
|
|
||||||
"lyric.User",
|
|
||||||
related_name="posts",
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
)
|
|
||||||
|
|
||||||
shared_with = models.ManyToManyField(
|
|
||||||
"lyric.User",
|
|
||||||
related_name="shared_posts",
|
|
||||||
blank=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self.lines.first().text
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("view_post", args=[self.id])
|
|
||||||
|
|
||||||
class Line(models.Model):
|
|
||||||
text = models.TextField(default="")
|
|
||||||
post = models.ForeignKey(Post, default=None, on_delete=models.CASCADE, related_name="lines")
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ("id",)
|
|
||||||
unique_together = ("post", "text")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.text
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from apps.dashboard.forms import (
|
from apps.billboard.forms import (
|
||||||
DUPLICATE_LINE_ERROR,
|
DUPLICATE_LINE_ERROR,
|
||||||
EMPTY_LINE_ERROR,
|
EMPTY_LINE_ERROR,
|
||||||
ExistingPostLineForm,
|
ExistingPostLineForm,
|
||||||
LineForm,
|
LineForm,
|
||||||
)
|
)
|
||||||
from apps.dashboard.models import Line, Post
|
from apps.billboard.models import Line, Post
|
||||||
|
|
||||||
|
|
||||||
class LineFormTest(TestCase):
|
class LineFormTest(TestCase):
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from apps.dashboard.models import Line, Post
|
from apps.billboard.models import Line, Post
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
|
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ class LineModelTest(TestCase):
|
|||||||
class PostModelTest(TestCase):
|
class PostModelTest(TestCase):
|
||||||
def test_get_absolute_url(self):
|
def test_get_absolute_url(self):
|
||||||
mypost = Post.objects.create()
|
mypost = Post.objects.create()
|
||||||
self.assertEqual(mypost.get_absolute_url(), f"/dashboard/post/{mypost.id}/")
|
self.assertEqual(mypost.get_absolute_url(), f"/billboard/post/{mypost.id}/")
|
||||||
|
|
||||||
def test_post_lines_order(self):
|
def test_post_lines_order(self):
|
||||||
post1 = Post.objects.create()
|
post1 = Post.objects.create()
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ from django.urls import reverse
|
|||||||
from django.utils import html, timezone
|
from django.utils import html, timezone
|
||||||
|
|
||||||
from apps.applets.models import Applet, UserApplet
|
from apps.applets.models import Applet, UserApplet
|
||||||
from apps.dashboard.forms import (
|
from apps.billboard.forms import (
|
||||||
DUPLICATE_LINE_ERROR,
|
DUPLICATE_LINE_ERROR,
|
||||||
EMPTY_LINE_ERROR,
|
EMPTY_LINE_ERROR,
|
||||||
)
|
)
|
||||||
from apps.dashboard.models import Line, Post
|
from apps.billboard.models import Line, Post
|
||||||
from apps.drama.models import Note
|
from apps.drama.models import Note
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
|
|
||||||
@@ -37,19 +37,19 @@ class NewPostTest(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_can_save_a_POST_request(self):
|
def test_can_save_a_POST_request(self):
|
||||||
self.client.post("/dashboard/new_post", data={"text": "A new post line"})
|
self.client.post("/billboard/new-post", data={"text": "A new post line"})
|
||||||
self.assertEqual(Line.objects.count(), 1)
|
self.assertEqual(Line.objects.count(), 1)
|
||||||
new_line = Line.objects.get()
|
new_line = Line.objects.get()
|
||||||
self.assertEqual(new_line.text, "A new post line")
|
self.assertEqual(new_line.text, "A new post line")
|
||||||
|
|
||||||
def test_redirects_after_POST(self):
|
def test_redirects_after_POST(self):
|
||||||
response = self.client.post("/dashboard/new_post", data={"text": "A new post line"})
|
response = self.client.post("/billboard/new-post", data={"text": "A new post line"})
|
||||||
new_post = Post.objects.get()
|
new_post = Post.objects.get()
|
||||||
self.assertRedirects(response, f"/dashboard/post/{new_post.id}/")
|
self.assertRedirects(response, f"/billboard/post/{new_post.id}/")
|
||||||
|
|
||||||
# Post invalid input helper
|
# Post invalid input helper
|
||||||
def post_invalid_input(self):
|
def post_invalid_input(self):
|
||||||
return self.client.post("/dashboard/new_post", data={"text": ""})
|
return self.client.post("/billboard/new-post", data={"text": ""})
|
||||||
|
|
||||||
def test_for_invalid_input_nothing_saved_to_db(self):
|
def test_for_invalid_input_nothing_saved_to_db(self):
|
||||||
self.post_invalid_input()
|
self.post_invalid_input()
|
||||||
@@ -68,12 +68,12 @@ class NewPostTest(TestCase):
|
|||||||
class PostViewTest(TestCase):
|
class PostViewTest(TestCase):
|
||||||
def test_uses_post_template(self):
|
def test_uses_post_template(self):
|
||||||
mypost = Post.objects.create()
|
mypost = Post.objects.create()
|
||||||
response = self.client.get(f"/dashboard/post/{mypost.id}/")
|
response = self.client.get(f"/billboard/post/{mypost.id}/")
|
||||||
self.assertTemplateUsed(response, "apps/dashboard/post.html")
|
self.assertTemplateUsed(response, "apps/billboard/post.html")
|
||||||
|
|
||||||
def test_renders_input_form(self):
|
def test_renders_input_form(self):
|
||||||
mypost = Post.objects.create()
|
mypost = Post.objects.create()
|
||||||
url = f"/dashboard/post/{mypost.id}/"
|
url = f"/billboard/post/{mypost.id}/"
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
parsed = lxml.html.fromstring(response.content)
|
parsed = lxml.html.fromstring(response.content)
|
||||||
forms = parsed.cssselect("form[method=POST]")
|
forms = parsed.cssselect("form[method=POST]")
|
||||||
@@ -90,7 +90,7 @@ class PostViewTest(TestCase):
|
|||||||
other_post = Post.objects.create()
|
other_post = Post.objects.create()
|
||||||
Line.objects.create(text="other post line", post=other_post)
|
Line.objects.create(text="other post line", post=other_post)
|
||||||
# When/Act
|
# When/Act
|
||||||
response = self.client.get(f"/dashboard/post/{correct_post.id}/")
|
response = self.client.get(f"/billboard/post/{correct_post.id}/")
|
||||||
# Then/Assert
|
# Then/Assert
|
||||||
self.assertContains(response, "itemey 1")
|
self.assertContains(response, "itemey 1")
|
||||||
self.assertContains(response, "itemey 2")
|
self.assertContains(response, "itemey 2")
|
||||||
@@ -101,7 +101,7 @@ class PostViewTest(TestCase):
|
|||||||
correct_post = Post.objects.create()
|
correct_post = Post.objects.create()
|
||||||
|
|
||||||
self.client.post(
|
self.client.post(
|
||||||
f"/dashboard/post/{correct_post.id}/",
|
f"/billboard/post/{correct_post.id}/",
|
||||||
data={"text": "A new line for an existing post"},
|
data={"text": "A new line for an existing post"},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -115,16 +115,16 @@ class PostViewTest(TestCase):
|
|||||||
correct_post = Post.objects.create()
|
correct_post = Post.objects.create()
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
f"/dashboard/post/{correct_post.id}/",
|
f"/billboard/post/{correct_post.id}/",
|
||||||
data={"text": "A new line for an existing post"},
|
data={"text": "A new line for an existing post"},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertRedirects(response, f"/dashboard/post/{correct_post.id}/")
|
self.assertRedirects(response, f"/billboard/post/{correct_post.id}/")
|
||||||
|
|
||||||
# Post invalid input helper
|
# Post invalid input helper
|
||||||
def post_invalid_input(self):
|
def post_invalid_input(self):
|
||||||
mypost = Post.objects.create()
|
mypost = Post.objects.create()
|
||||||
return self.client.post(f"/dashboard/post/{mypost.id}/", data={"text": ""})
|
return self.client.post(f"/billboard/post/{mypost.id}/", data={"text": ""})
|
||||||
|
|
||||||
def test_for_invalid_input_nothing_saved_to_db(self):
|
def test_for_invalid_input_nothing_saved_to_db(self):
|
||||||
self.post_invalid_input()
|
self.post_invalid_input()
|
||||||
@@ -133,7 +133,7 @@ class PostViewTest(TestCase):
|
|||||||
def test_for_invalid_input_renders_post_template(self):
|
def test_for_invalid_input_renders_post_template(self):
|
||||||
response = self.post_invalid_input()
|
response = self.post_invalid_input()
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTemplateUsed(response, "apps/dashboard/post.html")
|
self.assertTemplateUsed(response, "apps/billboard/post.html")
|
||||||
|
|
||||||
def test_for_invalid_input_shows_error_on_page(self):
|
def test_for_invalid_input_shows_error_on_page(self):
|
||||||
response = self.post_invalid_input()
|
response = self.post_invalid_input()
|
||||||
@@ -150,46 +150,46 @@ class PostViewTest(TestCase):
|
|||||||
Line.objects.create(post=post1, text="lorem ipsum")
|
Line.objects.create(post=post1, text="lorem ipsum")
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
f"/dashboard/post/{post1.id}/",
|
f"/billboard/post/{post1.id}/",
|
||||||
data={"text": "lorem ipsum"},
|
data={"text": "lorem ipsum"},
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_error = html.escape(DUPLICATE_LINE_ERROR)
|
expected_error = html.escape(DUPLICATE_LINE_ERROR)
|
||||||
self.assertContains(response, expected_error)
|
self.assertContains(response, expected_error)
|
||||||
self.assertTemplateUsed(response, "apps/dashboard/post.html")
|
self.assertTemplateUsed(response, "apps/billboard/post.html")
|
||||||
self.assertEqual(Line.objects.all().count(), 1)
|
self.assertEqual(Line.objects.all().count(), 1)
|
||||||
|
|
||||||
class MyPostsTest(TestCase):
|
class MyPostsTest(TestCase):
|
||||||
def test_my_posts_url_renders_my_posts_template(self):
|
def test_my_posts_url_renders_my_posts_template(self):
|
||||||
user = User.objects.create(email="a@b.cde")
|
user = User.objects.create(email="a@b.cde")
|
||||||
self.client.force_login(user)
|
self.client.force_login(user)
|
||||||
response = self.client.get(f"/dashboard/users/{user.id}/")
|
response = self.client.get(f"/billboard/users/{user.id}/")
|
||||||
self.assertTemplateUsed(response, "apps/dashboard/my_posts.html")
|
self.assertTemplateUsed(response, "apps/billboard/my_posts.html")
|
||||||
|
|
||||||
def test_passes_correct_owner_to_template(self):
|
def test_passes_correct_owner_to_template(self):
|
||||||
User.objects.create(email="wrongowner@example.com")
|
User.objects.create(email="wrongowner@example.com")
|
||||||
correct_user = User.objects.create(email="a@b.cde")
|
correct_user = User.objects.create(email="a@b.cde")
|
||||||
self.client.force_login(correct_user)
|
self.client.force_login(correct_user)
|
||||||
response = self.client.get(f"/dashboard/users/{correct_user.id}/")
|
response = self.client.get(f"/billboard/users/{correct_user.id}/")
|
||||||
self.assertEqual(response.context["owner"], correct_user)
|
self.assertEqual(response.context["owner"], correct_user)
|
||||||
|
|
||||||
def test_post_owner_is_saved_if_user_is_authenticated(self):
|
def test_post_owner_is_saved_if_user_is_authenticated(self):
|
||||||
user = User.objects.create(email="a@b.cde")
|
user = User.objects.create(email="a@b.cde")
|
||||||
self.client.force_login(user)
|
self.client.force_login(user)
|
||||||
self.client.post("/dashboard/new_post", data={"text": "new line"})
|
self.client.post("/billboard/new-post", data={"text": "new line"})
|
||||||
new_post = Post.objects.get()
|
new_post = Post.objects.get()
|
||||||
self.assertEqual(new_post.owner, user)
|
self.assertEqual(new_post.owner, user)
|
||||||
|
|
||||||
def test_my_posts_redirects_if_not_logged_in(self):
|
def test_my_posts_redirects_if_not_logged_in(self):
|
||||||
user = User.objects.create(email="a@b.cde")
|
user = User.objects.create(email="a@b.cde")
|
||||||
response = self.client.get(f"/dashboard/users/{user.id}/")
|
response = self.client.get(f"/billboard/users/{user.id}/")
|
||||||
self.assertRedirects(response, "/")
|
self.assertRedirects(response, "/")
|
||||||
|
|
||||||
def test_my_posts_returns_403_for_wrong_user(self):
|
def test_my_posts_returns_403_for_wrong_user(self):
|
||||||
user1 = User.objects.create(email="a@b.cde")
|
user1 = User.objects.create(email="a@b.cde")
|
||||||
user2 = User.objects.create(email="wrongowner@example.com")
|
user2 = User.objects.create(email="wrongowner@example.com")
|
||||||
self.client.force_login(user2)
|
self.client.force_login(user2)
|
||||||
response = self.client.get(f"/dashboard/users/{user1.id}/")
|
response = self.client.get(f"/billboard/users/{user1.id}/")
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
class SharePostTest(TestCase):
|
class SharePostTest(TestCase):
|
||||||
@@ -197,16 +197,16 @@ class SharePostTest(TestCase):
|
|||||||
our_post = Post.objects.create()
|
our_post = Post.objects.create()
|
||||||
alice = User.objects.create(email="alice@example.com")
|
alice = User.objects.create(email="alice@example.com")
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
f"/dashboard/post/{our_post.id}/share_post",
|
f"/billboard/post/{our_post.id}/share-post",
|
||||||
data={"recipient": "alice@example.com"},
|
data={"recipient": "alice@example.com"},
|
||||||
)
|
)
|
||||||
self.assertRedirects(response, f"/dashboard/post/{our_post.id}/")
|
self.assertRedirects(response, f"/billboard/post/{our_post.id}/")
|
||||||
|
|
||||||
def test_post_with_email_adds_user_to_shared_with(self):
|
def test_post_with_email_adds_user_to_shared_with(self):
|
||||||
our_post = Post.objects.create()
|
our_post = Post.objects.create()
|
||||||
alice = User.objects.create(email="alice@example.com")
|
alice = User.objects.create(email="alice@example.com")
|
||||||
self.client.post(
|
self.client.post(
|
||||||
f"/dashboard/post/{our_post.id}/share_post",
|
f"/billboard/post/{our_post.id}/share-post",
|
||||||
data={"recipient": "alice@example.com"},
|
data={"recipient": "alice@example.com"},
|
||||||
)
|
)
|
||||||
self.assertIn(alice, our_post.shared_with.all())
|
self.assertIn(alice, our_post.shared_with.all())
|
||||||
@@ -214,12 +214,12 @@ class SharePostTest(TestCase):
|
|||||||
def test_post_with_nonexistent_email_redirects_to_post(self):
|
def test_post_with_nonexistent_email_redirects_to_post(self):
|
||||||
our_post = Post.objects.create()
|
our_post = Post.objects.create()
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
f"/dashboard/post/{our_post.id}/share_post",
|
f"/billboard/post/{our_post.id}/share-post",
|
||||||
data={"recipient": "nobody@example.com"},
|
data={"recipient": "nobody@example.com"},
|
||||||
)
|
)
|
||||||
self.assertRedirects(
|
self.assertRedirects(
|
||||||
response,
|
response,
|
||||||
f"/dashboard/post/{our_post.id}/",
|
f"/billboard/post/{our_post.id}/",
|
||||||
fetch_redirect_response=False,
|
fetch_redirect_response=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ class SharePostTest(TestCase):
|
|||||||
owner = User.objects.create(email="owner@example.com")
|
owner = User.objects.create(email="owner@example.com")
|
||||||
our_post = Post.objects.create(owner=owner)
|
our_post = Post.objects.create(owner=owner)
|
||||||
self.client.force_login(owner)
|
self.client.force_login(owner)
|
||||||
self.client.post(reverse("share_post", args=[our_post.id]),
|
self.client.post(reverse("billboard:share_post", args=[our_post.id]),
|
||||||
data={"recipient": "owner@example.com"})
|
data={"recipient": "owner@example.com"})
|
||||||
self.assertNotIn(owner, our_post.shared_with.all())
|
self.assertNotIn(owner, our_post.shared_with.all())
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ class SharePostTest(TestCase):
|
|||||||
def test_share_post_shows_privacy_safe_message(self):
|
def test_share_post_shows_privacy_safe_message(self):
|
||||||
our_post = Post.objects.create()
|
our_post = Post.objects.create()
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
f"/dashboard/post/{our_post.id}/share_post",
|
f"/billboard/post/{our_post.id}/share-post",
|
||||||
data={"recipient": "nobody@example.com"},
|
data={"recipient": "nobody@example.com"},
|
||||||
follow=True,
|
follow=True,
|
||||||
)
|
)
|
||||||
@@ -251,20 +251,20 @@ class ViewAuthPostTest(TestCase):
|
|||||||
self.our_post = Post.objects.create(owner=self.owner)
|
self.our_post = Post.objects.create(owner=self.owner)
|
||||||
|
|
||||||
def test_anonymous_user_is_redirected(self):
|
def test_anonymous_user_is_redirected(self):
|
||||||
response = self.client.get(reverse("view_post", args=[self.our_post.id]))
|
response = self.client.get(reverse("billboard:view_post", args=[self.our_post.id]))
|
||||||
self.assertRedirects(response, "/", fetch_redirect_response=False)
|
self.assertRedirects(response, "/", fetch_redirect_response=False)
|
||||||
|
|
||||||
def test_non_owner_non_shared_user_gets_403(self):
|
def test_non_owner_non_shared_user_gets_403(self):
|
||||||
stranger = User.objects.create(email="stranger@example.com")
|
stranger = User.objects.create(email="stranger@example.com")
|
||||||
self.client.force_login(stranger)
|
self.client.force_login(stranger)
|
||||||
response = self.client.get(reverse("view_post", args=[self.our_post.id]))
|
response = self.client.get(reverse("billboard:view_post", args=[self.our_post.id]))
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_shared_with_user_can_access_post(self):
|
def test_shared_with_user_can_access_post(self):
|
||||||
guest = User.objects.create(email="guest@example.com")
|
guest = User.objects.create(email="guest@example.com")
|
||||||
self.our_post.shared_with.add(guest)
|
self.our_post.shared_with.add(guest)
|
||||||
self.client.force_login(guest)
|
self.client.force_login(guest)
|
||||||
response = self.client.get(reverse("view_post", args=[self.our_post.id]))
|
response = self.client.get(reverse("billboard:view_post", args=[self.our_post.id]))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@override_settings(COMPRESS_ENABLED=False)
|
@override_settings(COMPRESS_ENABLED=False)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
from apps.dashboard.forms import (
|
from apps.billboard.forms import (
|
||||||
EMPTY_LINE_ERROR,
|
EMPTY_LINE_ERROR,
|
||||||
LineForm,
|
LineForm,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
from apps.dashboard.models import Line
|
from apps.billboard.models import Line
|
||||||
|
|
||||||
|
|
||||||
class SimpleLineModelTest(SimpleTestCase):
|
class SimpleLineModelTest(SimpleTestCase):
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ from django.urls import path
|
|||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('new_post', views.new_post, name='new_post'),
|
# Post/Line CRUD has moved to apps.billboard.urls (`billboard:` namespace).
|
||||||
path('post/<uuid:post_id>/', views.view_post, name='view_post'),
|
|
||||||
path('post/<uuid:post_id>/share_post', views.share_post, name="share_post"),
|
|
||||||
path('set_palette', views.set_palette, name='set_palette'),
|
path('set_palette', views.set_palette, name='set_palette'),
|
||||||
path('set_profile', views.set_profile, name='set_profile'),
|
path('set_profile', views.set_profile, name='set_profile'),
|
||||||
path('users/<uuid:user_id>/', views.my_posts, name='my_posts'),
|
|
||||||
path('toggle_applets', views.toggle_applets, name="toggle_applets"),
|
path('toggle_applets', views.toggle_applets, name="toggle_applets"),
|
||||||
path('wallet/', views.wallet, name='wallet'),
|
path('wallet/', views.wallet, name='wallet'),
|
||||||
path('wallet/toggle-applets', views.toggle_wallet_applets, name='toggle_wallet_applets'),
|
path('wallet/toggle-applets', views.toggle_wallet_applets, name='toggle_wallet_applets'),
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ from django.utils import timezone
|
|||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
|
|
||||||
from apps.applets.utils import applet_context, apply_applet_toggle
|
from apps.applets.utils import applet_context, apply_applet_toggle
|
||||||
from apps.dashboard.forms import ExistingPostLineForm, LineForm
|
|
||||||
from apps.dashboard.models import Line, Post
|
|
||||||
from apps.drama.models import Note
|
from apps.drama.models import Note
|
||||||
from apps.epic.utils import _compute_distinctions
|
from apps.epic.utils import _compute_distinctions
|
||||||
from apps.lyric.models import PaymentMethod, Token, User, Wallet
|
from apps.lyric.models import PaymentMethod, Token, User, Wallet
|
||||||
@@ -85,16 +83,6 @@ def _unlocked_palettes_for_user(user):
|
|||||||
return base
|
return base
|
||||||
|
|
||||||
|
|
||||||
def _recent_posts(user, limit=3):
|
|
||||||
return (
|
|
||||||
Post
|
|
||||||
.objects
|
|
||||||
.filter(Q(owner=user) | Q(shared_with=user))
|
|
||||||
.annotate(last_line=Max('lines__id'))
|
|
||||||
.order_by('-last_line')
|
|
||||||
.distinct()[:limit]
|
|
||||||
)
|
|
||||||
|
|
||||||
def home_page(request):
|
def home_page(request):
|
||||||
context = {
|
context = {
|
||||||
"palettes": _palettes_for_user(request.user),
|
"palettes": _palettes_for_user(request.user),
|
||||||
@@ -104,62 +92,10 @@ def home_page(request):
|
|||||||
context["applets"] = applet_context(request.user, "dashboard")
|
context["applets"] = applet_context(request.user, "dashboard")
|
||||||
return render(request, "apps/dashboard/home.html", context)
|
return render(request, "apps/dashboard/home.html", context)
|
||||||
|
|
||||||
def new_post(request):
|
|
||||||
form = LineForm(data=request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
nupost = Post.objects.create()
|
|
||||||
if request.user.is_authenticated:
|
|
||||||
nupost.owner = request.user
|
|
||||||
nupost.save()
|
|
||||||
form.save(for_post=nupost)
|
|
||||||
return redirect(nupost)
|
|
||||||
else:
|
|
||||||
context = {
|
|
||||||
"form": form,
|
|
||||||
"page_class": "page-billboard",
|
|
||||||
}
|
|
||||||
if request.user.is_authenticated:
|
|
||||||
context["applets"] = applet_context(request.user, "billboard")
|
|
||||||
context["recent_posts"] = _recent_posts(request.user)
|
|
||||||
return render(request, "apps/billboard/billboard.html", context)
|
|
||||||
|
|
||||||
def view_post(request, post_id):
|
# Post / Line CRUD lives in apps.billboard.views since the Post + Line models
|
||||||
our_post = Post.objects.get(id=post_id)
|
# moved to apps.billboard.models.
|
||||||
|
|
||||||
if our_post.owner:
|
|
||||||
if not request.user.is_authenticated:
|
|
||||||
return redirect("/")
|
|
||||||
if request.user != our_post.owner and request.user not in our_post.shared_with.all():
|
|
||||||
return HttpResponseForbidden()
|
|
||||||
|
|
||||||
form = ExistingPostLineForm(for_post=our_post)
|
|
||||||
|
|
||||||
if request.method == "POST":
|
|
||||||
form = ExistingPostLineForm(for_post=our_post, data=request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
form.save()
|
|
||||||
return redirect(our_post)
|
|
||||||
return render(request, "apps/dashboard/post.html", {"post": our_post, "form": form})
|
|
||||||
|
|
||||||
def my_posts(request, user_id):
|
|
||||||
owner = User.objects.get(id=user_id)
|
|
||||||
if not request.user.is_authenticated:
|
|
||||||
return redirect("/")
|
|
||||||
if request.user.id != owner.id:
|
|
||||||
return HttpResponseForbidden()
|
|
||||||
return render(request, "apps/dashboard/my_posts.html", {"owner": owner})
|
|
||||||
|
|
||||||
def share_post(request, post_id):
|
|
||||||
our_post = Post.objects.get(id=post_id)
|
|
||||||
try:
|
|
||||||
recipient = User.objects.get(email=request.POST["recipient"])
|
|
||||||
if recipient == request.user:
|
|
||||||
return redirect(our_post)
|
|
||||||
our_post.shared_with.add(recipient)
|
|
||||||
except User.DoesNotExist:
|
|
||||||
pass
|
|
||||||
messages.success(request, "An invite has been sent if that address is registered.")
|
|
||||||
return redirect(our_post)
|
|
||||||
|
|
||||||
@login_required(login_url="/")
|
@login_required(login_url="/")
|
||||||
def set_palette(request):
|
def set_palette(request):
|
||||||
|
|||||||
18
src/apps/drama/migrations/0004_alter_gameevent_verb.py
Normal file
18
src/apps/drama/migrations/0004_alter_gameevent_verb.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-05-08 21:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('drama', '0003_grant_super_notes_to_existing_superusers'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='gameevent',
|
||||||
|
name='verb',
|
||||||
|
field=models.CharField(choices=[('room_created', 'Room created'), ('slot_reserved', 'Gate slot reserved'), ('slot_filled', 'Gate slot filled'), ('slot_returned', 'Gate slot returned'), ('slot_released', 'Gate slot released'), ('invite_sent', 'Invite sent'), ('role_select_started', 'Role select started'), ('role_selected', 'Role selected'), ('roles_revealed', 'Roles revealed'), ('sig_ready', 'Sig claim staked'), ('sig_unready', 'Sig claim withdrawn'), ('sky_saved', 'Sky saved')], max_length=30),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -11,7 +11,7 @@ class MyPostsPage:
|
|||||||
self.test.browser.get(self.test.live_server_url)
|
self.test.browser.get(self.test.live_server_url)
|
||||||
user = User.objects.get(email=email)
|
user = User.objects.get(email=email)
|
||||||
self.test.browser.get(
|
self.test.browser.get(
|
||||||
self.test.live_server_url + f'/dashboard/users/{user.id}/'
|
self.test.live_server_url + f'/billboard/users/{user.id}/'
|
||||||
)
|
)
|
||||||
self.test.wait_for(
|
self.test.wait_for(
|
||||||
lambda: self.test.assertIn(
|
lambda: self.test.assertIn(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
id="id_applet_my_posts"
|
id="id_applet_my_posts"
|
||||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||||
>
|
>
|
||||||
<h2><a href="{% url 'my_posts' user.id %}" class="my-posts-main">My Posts</a></h2>
|
<h2><a href="{% url 'billboard:my_posts' user.id %}" class="my-posts-main">My Posts</a></h2>
|
||||||
<div class="my-posts-container">
|
<div class="my-posts-container">
|
||||||
<ul>
|
<ul>
|
||||||
{% for post in recent_posts %}
|
{% for post in recent_posts %}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||||
>
|
>
|
||||||
<h2>New Post</h2>
|
<h2>New Post</h2>
|
||||||
{% url "new_post" as form_action %}
|
{% url "billboard:new_post" as form_action %}
|
||||||
{% include "apps/dashboard/_partials/_form.html" with form=form form_action=form_action %}
|
{% include "apps/dashboard/_partials/_form.html" with form=form form_action=form_action %}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
|
|
||||||
{% block extra_header %}
|
{% block extra_header %}
|
||||||
{% url "view_post" post.id as form_action %}
|
{% url "billboard:view_post" post.id as form_action %}
|
||||||
{% include "apps/dashboard/_partials/_form.html" with form=form form_action=form_action %}
|
{% include "apps/dashboard/_partials/_form.html" with form=form form_action=form_action %}
|
||||||
{% endblock extra_header %}
|
{% endblock extra_header %}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
|
|
||||||
<form method="POST" action="{% url "share_post" post.id %}">
|
<form method="POST" action="{% url "billboard:share_post" post.id %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input
|
<input
|
||||||
id="id_recipient"
|
id="id_recipient"
|
||||||
Reference in New Issue
Block a user