fix overflow + editing + post list order

This commit is contained in:
trinkey 2025-01-04 15:41:02 -05:00
parent 8a648d23c4
commit 616dcd49a1
12 changed files with 175 additions and 57 deletions

0
manage.py Executable file → Normal file
View file

View file

@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2025-01-04 20:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tblog', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='tbpost',
name='edited_at',
field=models.IntegerField(null=True),
),
]

View file

@ -20,6 +20,7 @@ class tBPost(models.Model):
title = models.TextField(max_length=1_000) title = models.TextField(max_length=1_000)
content = models.TextField(max_length=500_000) content = models.TextField(max_length=500_000)
timestamp = models.IntegerField() timestamp = models.IntegerField()
edited_at = models.IntegerField(null=True)
text_format = models.CharField(max_length=10) # plain, mono, markdown, html ... (more to come?) text_format = models.CharField(max_length=10) # plain, mono, markdown, html ... (more to come?)
class Meta: class Meta:

16
tblog/static/css/blog.css Normal file
View file

@ -0,0 +1,16 @@
#real-blog-container[data-blog-format="mono"] pre,
#real-blog-container[data-blog-format="plain"] pre {
white-space: pre-wrap;
overflow-x: scroll;
}
#real-blog-container[data-blog-format="html"],
#real-blog-container[data-blog-format="markdown"] {
width: 100%;
overflow-x: scroll;
}
#blog-container {
max-width: 1000px;
width: calc(90vw - 40px);
}

View file

@ -8,8 +8,18 @@
{% block body %} {% block body %}
<h1>{{ blog.title }}</h1> <h1>{{ blog.title }}</h1>
<div><b>By <a href="/blog/{{ blog.u_by.username }}">{{ blog.u_by.username }}</a></b></div> <div><b>By <a href="/blog/{{ blog.u_by.username }}">{{ blog.u_by.username }}</a></b></div>
<small data-timestamp="{{ blog.timestamp }}"></small>
<small>
<span data-timestamp="{{ blog.timestamp }}"></span>
{% if blog.edited_at %}
- Edited
<span data-timestamp="{{ blog.edited_at }}"></span>
{% endif %}
</small>
{% if username == blog.u_by.username %} {% if username == blog.u_by.username %}
<p><a href="/blog/{{ username }}/{{ blog.url }}/edit/">Edit post</a></p>
<form method="POST"> <form method="POST">
{% csrf_token %} {% csrf_token %}
<p> <p>
@ -20,7 +30,7 @@
</form> </form>
{% endif %} {% endif %}
<hr> <hr>
<div id="blog-container" style="max-width: 1000px; width: 90vw;" class="inline-block left" data-format="{{ blog.text_format }}" data-raw="{{ blog.content }}"><i>Loading...</i></div> <div id="blog-container" class="inline-block left" data-format="{{ blog.text_format }}" data-raw="{{ blog.content }}"><i>Loading...</i></div>
<script> <script>
const blog = document.getElementById("blog-container"); const blog = document.getElementById("blog-container");

View file

@ -4,16 +4,18 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.37.1/ace.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.37.1/ace.min.js"></script>
{% endif %} {% endif %}
<link rel="stylesheet" href="/static/css/blog.css">
<script> <script>
function toHTML(format, raw) { function toHTML(format, raw) {
if (format == "markdown") { if (format == "markdown") {
return DOMPurify.sanitize(marked.parse(raw)); return `<div id="real-blog-container" data-blog-format="markdown">${DOMPurify.sanitize(marked.parse(raw))}</div>`;
} else if (format == "html") { } else if (format == "html") {
return DOMPurify.sanitize(raw); return `<div id="real-blog-container" data-blog-format="html">${DOMPurify.sanitize(raw)}</div>`;
} else if (format == "mono") { } else if (format == "mono") {
return `<pre class="no-margin">${raw.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")}</pre>`; return `<div id="real-blog-container" data-blog-format="mono"><pre class="no-margin">${raw.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")}</pre></div>`;
} else { } else {
return `<pre class="not-code no-margin">${raw.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")}</pre>`; return `<div id="real-blog-container" data-blog-format="plain"><pre class="not-code no-margin">${raw.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")}</pre></div>`;
} }
} }

View file

@ -8,7 +8,7 @@
max-width: 600px; max-width: 600px;
border: 1px solid rgb(var(--accent)); border: 1px solid rgb(var(--accent));
border-radius: 10px; border-radius: 10px;
margin-top: 18px; margin: 18px 0;
padding: 20px; padding: 20px;
text-align: left; text-align: left;
} }
@ -31,7 +31,13 @@
<hr> <hr>
<a class="fake-link blog-post" href="/blog/{{ username }}/{{ post.url }}/"> <a class="fake-link blog-post" href="/blog/{{ username }}/{{ post.url }}/">
<h2 class="no-margin">{{ post.title }}</h2> <h2 class="no-margin">{{ post.title }}</h2>
<small data-timestamp="{{ post.timestamp }}"></small> <small>
<span data-timestamp="{{ post.timestamp }}"></span>
{% if post.edited_at %}
- Edited
<span data-timestamp="{{ post.edited_at }}"></span>
{% endif %}
</small>
</a> </a>
{% empty %} {% empty %}
<hr> <hr>

View file

@ -19,7 +19,7 @@
{% if error %}<p class="error">{{ error }}</p>{% endif %} {% if error %}<p class="error">{{ error }}</p>{% endif %}
<p>Characters: <b><span id="character-count" data-localize-number="0">0</span>/<span data-localize-number="500000">500000</span></b></p> <p>Characters: <b><span id="character-count">0</span>/<span data-localize-number="500000">500000</span></b></p>
<p><b>Configuration:</b></p> <p><b>Configuration:</b></p>
<p> <p>
@ -43,12 +43,10 @@
</p> </p>
<p> <p>
<label for="url">{{ config.services.blog.url.pub }}/blog/{{ username }}/</label <label for="url">{{ config.services.blog.url.pub }}/blog/{{ username }}/</label><input maxlength="1000" placeholder="blog-url" required name="url" id="url" value="{{ repopulate.url }}">
><input maxlength="1000" placeholder="blog-url" required name="url" id="url" value="{{ repopulate.url }}">
</p> </p>
<p><input type="submit" value="Create Blog Post"></p> <p><input type="submit" value="{% if editing %}Confirm Edits{% else %}Create Blog Post{% endif %}"></p>
<small>(blog posts can't be changed after posting)</small>
</form> </form>
<script src="/static/js/write.js?v={{ config.version_str }}"></script> <script src="/static/js/write.js?v={{ config.version_str }}"></script>

View file

@ -1,7 +1,7 @@
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from .views import auth, index, user, view_blog, write from .views import auth, edit_blog, index, user, view_blog, write
urlpatterns = [ urlpatterns = [
path("", index), path("", index),
@ -9,6 +9,7 @@ urlpatterns = [
path("create/", write), path("create/", write),
path("blog/<str:username>/", user), path("blog/<str:username>/", user),
path("blog/<str:username>/<str:url>/", view_blog), path("blog/<str:username>/<str:url>/", view_blog),
path("blog/<str:username>/<str:url>/edit/", edit_blog),
path("static/", include("tblog.views.static")), path("static/", include("tblog.views.static")),
path("django-admin/", admin.site.urls) path("django-admin/", admin.site.urls)
] ]

View file

@ -1 +1,2 @@
from .templates import auth, index, user, view_blog, write # noqa: F401 from .templates import (auth, edit_blog, index, user, view_blog, # noqa: F401
write)

View file

@ -22,6 +22,7 @@ file_associations = {
urlpatterns = [path(i, cache_control(**{"max-age": 60 * 60 * 24 * 30})(get_static_serve(i, file_associations[i.split(".")[-1]]))) for i in [ urlpatterns = [path(i, cache_control(**{"max-age": 60 * 60 * 24 * 30})(get_static_serve(i, file_associations[i.split(".")[-1]]))) for i in [
"css/ace.css", "css/ace.css",
"css/blog.css",
"css/write.css", "css/write.css",
"js/write.js" "js/write.js"
]] ]]

View file

@ -38,51 +38,51 @@ def index(request: WSGIRequest) -> HttpResponse:
def write(request: WSGIRequest) -> HttpResponse: def write(request: WSGIRequest) -> HttpResponse:
username = get_username(request) username = get_username(request)
if username: if not username:
repopulate = {}
error = None
if request.method == "POST":
url = (request.POST.get("url") or "").strip().replace(" ", "-").lower()
title = (request.POST.get("title") or "").strip()
content = (request.POST.get("raw") or "").strip()
fmt = request.POST.get("format")
repopulate = {
"url": url,
"title": title,
"content": content,
"format": fmt
}
if not (url and title and content) or fmt not in ["plain", "mono", "markdown", "html"] or len(url) > 250 or len(title) > 1_000 or len(content) > 500_000:
error = "Invalid input(s)"
else:
try:
tBPost.objects.create(
u_by=get_user_object(username, i_promise_this_user_exists=True),
url=request.POST.get("url"),
title=request.POST.get("title"),
content=request.POST.get("raw"),
timestamp=math.floor(time.time()),
text_format=request.POST.get("format")
)
except IntegrityError:
error = f"Url '{url}' already in use"
else:
return HttpResponseRedirect(f"/blog/{username}/{escape_url(url)}/")
return render_template( return render_template(
request, "write.html", request, "noauth/index.html"
title="Writing",
username=username,
error=error,
repopulate=repopulate
) )
repopulate = {}
error = None
if request.method == "POST":
url = (request.POST.get("url") or "").strip().replace(" ", "-").lower()
title = (request.POST.get("title") or "").strip()
content = (request.POST.get("raw") or "").strip()
fmt = request.POST.get("format")
repopulate = {
"url": url,
"title": title,
"content": content,
"format": fmt
}
if not (url and title and content) or fmt not in ["plain", "mono", "markdown", "html"] or len(url) > 250 or len(title) > 1_000 or len(content) > 500_000:
error = "Invalid input(s)"
else:
try:
tBPost.objects.create(
u_by=get_user_object(username, i_promise_this_user_exists=True),
url=url,
title=title,
content=content,
timestamp=math.floor(time.time()),
text_format=fmt
)
except IntegrityError:
error = f"Url '{url}' already in use"
else:
return HttpResponseRedirect(f"/blog/{username}/{escape_url(url)}/")
return render_template( return render_template(
request, "noauth/index.html" request, "write.html",
title="Writing",
username=username,
error=error,
repopulate=repopulate
) )
def view_blog(request: WSGIRequest, username: str, url: str) -> HttpResponse: def view_blog(request: WSGIRequest, username: str, url: str) -> HttpResponse:
@ -109,6 +109,70 @@ def view_blog(request: WSGIRequest, username: str, url: str) -> HttpResponse:
username=self_username username=self_username
) )
def edit_blog(request: WSGIRequest, username: str, url: str) -> HttpResponse:
try:
blog = tBPost.objects.get(
u_by=get_user_object(username),
url=url
)
except tBPost.DoesNotExist:
return render_template(
request, "404.html"
)
self_username = get_username(request)
if username != self_username:
return render_template(
request, "404.html"
)
error = None
repopulate = {
"url": blog.url,
"title": blog.title,
"content": blog.content,
"format": blog.text_format
}
if request.method == "POST":
url = (request.POST.get("url") or "").strip().replace(" ", "-").lower() or blog.url
title = (request.POST.get("title") or "").strip() or blog.title
content = (request.POST.get("raw") or "").strip() or blog.content
fmt = request.POST.get("format") or blog.text_format
repopulate = {
"url": url,
"title": title,
"content": content,
"format": fmt
}
if not (url and title and content) or fmt not in ["plain", "mono", "markdown", "html"] or len(url) > 250 or len(title) > 1_000 or len(content) > 500_000:
error = "Invalid input(s)"
else:
try:
blog.url = url
blog.title = title
blog.content = content
blog.edited_at = math.floor(time.time())
blog.text_format = fmt
blog.save()
except IntegrityError:
error = f"Url '{url}' already in use"
else:
return HttpResponseRedirect(f"/blog/{username}/{escape_url(url)}/")
return render_template(
request, "write.html",
title="Editing",
editing=True,
username=username,
error=error,
repopulate=repopulate
)
def user(request: WSGIRequest, username: str) -> HttpResponse: def user(request: WSGIRequest, username: str) -> HttpResponse:
user = get_user_object(username) user = get_user_object(username)
@ -117,7 +181,7 @@ def user(request: WSGIRequest, username: str) -> HttpResponse:
request, "user.html", request, "user.html",
username=user.username, username=user.username,
self_username=get_username(request), self_username=get_username(request),
posts=user.posts.all() # type: ignore posts=user.posts.order_by("-id") # type: ignore
) )
return render_template( return render_template(