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)
content = models.TextField(max_length=500_000)
timestamp = models.IntegerField()
edited_at = models.IntegerField(null=True)
text_format = models.CharField(max_length=10) # plain, mono, markdown, html ... (more to come?)
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 %}
<h1>{{ blog.title }}</h1>
<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 %}
<p><a href="/blog/{{ username }}/{{ blog.url }}/edit/">Edit post</a></p>
<form method="POST">
{% csrf_token %}
<p>
@ -20,7 +30,7 @@
</form>
{% endif %}
<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>
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>
{% endif %}
<link rel="stylesheet" href="/static/css/blog.css">
<script>
function toHTML(format, raw) {
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") {
return DOMPurify.sanitize(raw);
return `<div id="real-blog-container" data-blog-format="html">${DOMPurify.sanitize(raw)}</div>`;
} 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 {
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;
border: 1px solid rgb(var(--accent));
border-radius: 10px;
margin-top: 18px;
margin: 18px 0;
padding: 20px;
text-align: left;
}
@ -31,7 +31,13 @@
<hr>
<a class="fake-link blog-post" href="/blog/{{ username }}/{{ post.url }}/">
<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>
{% empty %}
<hr>

View file

@ -19,7 +19,7 @@
{% 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>
@ -43,12 +43,10 @@
</p>
<p>
<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 }}">
<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 }}">
</p>
<p><input type="submit" value="Create Blog Post"></p>
<small>(blog posts can't be changed after posting)</small>
<p><input type="submit" value="{% if editing %}Confirm Edits{% else %}Create Blog Post{% endif %}"></p>
</form>
<script src="/static/js/write.js?v={{ config.version_str }}"></script>

View file

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

View file

@ -38,51 +38,51 @@ def index(request: WSGIRequest) -> HttpResponse:
def write(request: WSGIRequest) -> HttpResponse:
username = get_username(request)
if 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)}/")
if not username:
return render_template(
request, "write.html",
title="Writing",
username=username,
error=error,
repopulate=repopulate
request, "noauth/index.html"
)
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(
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:
@ -109,6 +109,70 @@ def view_blog(request: WSGIRequest, username: str, url: str) -> HttpResponse:
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:
user = get_user_object(username)
@ -117,7 +181,7 @@ def user(request: WSGIRequest, username: str) -> HttpResponse:
request, "user.html",
username=user.username,
self_username=get_username(request),
posts=user.posts.all() # type: ignore
posts=user.posts.order_by("-id") # type: ignore
)
return render_template(