2025.12.23 17:30:33 (cachyos.cmoser.eu)

This commit is contained in:
2025-12-23 17:30:33 +01:00
parent 3c4647ce49
commit c2e431e43b
14 changed files with 1175 additions and 569 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

View File

@@ -1,3 +1,5 @@
{ {
"python-envs.pythonProjects": [] "python-envs.pythonProjects": [],
"python-envs.defaultEnvManager": "ms-python.python:poetry",
"python-envs.defaultPackageManager": "ms-python.python:poetry"
} }

View File

@@ -122,6 +122,7 @@ TEMPLATES = [
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'tinywiki.context_processors.sidebar',
], ],
}, },
}, },
@@ -202,6 +203,12 @@ else:
print("Email backend not known falling back to console!",file=sys.stderr) print("Email backend not known falling back to console!",file=sys.stderr)
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
CSP_DEFAULT_SRC = [
"'self'",
"https://youtube.com",
]
_secret_key = LOCAL_DIR / "secret_key.py" _secret_key = LOCAL_DIR / "secret_key.py"
if _secret_key.is_file(): if _secret_key.is_file():
from .local import secret_key from .local import secret_key

1064
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,7 @@ dependencies = [
"pillow (>=11.3.0,<12.0.0)", "pillow (>=11.3.0,<12.0.0)",
"bbcode (>=1.1.0,<2.0.0)", "bbcode (>=1.1.0,<2.0.0)",
"markdown (>=3.9,<4.0)", "markdown (>=3.9,<4.0)",
"django-csp (>=4.0,<5.0)",
] ]
[tool.poetry] [tool.poetry]

View File

@@ -1,19 +1,39 @@
#!/bin/bash #!/bin/bash
: ${UWSGI_BUFFER_SIZE:=32768}
: ${UWSGI_SOCKET_NAME:=uwsgi.sock}
: ${UWSGI_SOCKET:=/data/run/${UWSGI_SOCKET_NAME}}
: ${UWSGI_PIDFILE_NAME:=uwsgi.pid}
: ${UWSGI_PIDFILE:=/data/run/${UWSGI_SOCKET_NAME}}
: ${UWSGI_PROCESSES:=4}
: ${HTTP_PORT:=8000}
: ${HTTP_ADDRESS:=0.0.0.0}
: ${HTTP_SOCKET:=${HTTP_ADDRESS}:${HTTP_PORT}}
: ${UWSGI_MAX_REQUESTS:=4000}
migrate_db() {
echo "Migrating database ..." echo "Migrating database ..."
poetry run python manage.py migrate poetry run python manage.py migrate
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Unable to migrate database!" >&2 echo "Unable to migrate database!" >&2
exit 5 exit 5
fi fi
}
collectstatic() {
echo "Running collectstatic ..." echo "Running collectstatic ..."
yes yes | poetry run python manage.py collectstatic yes yes | poetry run python manage.py collectstatic
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Unable to collect static files!" >&2 echo "Unable to collect static files!" >&2
exit 5 exit 5
fi fi
}
createsuperuser() {
poetry run python manage.py createsuperuser "$@"
}
if [ $# -eq 0 -o "$1" = "start" ]; then
if [ -z "$DEBUG" -o $DEBUG != "1" ]; then if [ -z "$DEBUG" -o $DEBUG != "1" ]; then
echo "Starting UWSGI server ..." echo "Starting UWSGI server ..."
venv="$(poetry env info | head -n 6 | grep Path | awk '{print $2}')" venv="$(poetry env info | head -n 6 | grep Path | awk '{print $2}')"
@@ -21,15 +41,15 @@ if [ -z "$DEBUG" -o $DEBUG != "1" ]; then
--chdir /app \ --chdir /app \
--module django_project.wsgi:application \ --module django_project.wsgi:application \
--master --pidfile /data/run/uwsgi.pid \ --master --pidfile /data/run/uwsgi.pid \
--http-socket '0.0.0.0:8000' \ --http-socket "$HTTP_SOCKET" \
--socket /data/run/uwsgi.sock \ --socket "$UWSGI_SOCKET" \
--processes 5 \ --processes $UWSGI_PROCESSES \
--harakiri 60 \ --harakiri 60 \
--max-requests 5000 \ --max-requests $UWSGI_MAX_REQUESTS \
--buffer-size $UWSGI_BUFFER_SIZE \
--vacuum \ --vacuum \
--venv "$venv" \ --venv "$venv" \
--home "$venv" --home "$venv"
rc=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "UWSGI Server not started" >&2 echo "UWSGI Server not started" >&2
@@ -37,10 +57,28 @@ if [ -z "$DEBUG" -o $DEBUG != "1" ]; then
fi fi
else else
echo "Starting development server ..." echo "Starting development server ..."
poetry run python manage.py runserver 0.0.0.0:8000 poetry run python manage.py runserver $HTTP_SOCKET
rc=$? rc=$?
if [ $rc -ne 0 ]; then if [ $rc -ne 0 ]; then
echo "Developemnt server was not started!" >&2 echo "Developemnt server was not started!" >&2
exit $rc exit $rc
fi fi
fi fi
elif [ "$1" == "init" -o "$1" == "update" ]; then
migrate_db
collectstatic
elif [ "$1" == "suinit" ]; then
shift
migrate_db
collectstatic
createsuperuser "$@"
elif [ "$1" == "migrate" ]; then
migrate_db
elif [ "$1" == "collectstatic" ]; then
collectstatic
elif [ "$1" == "createsuperuser" ]; then
shift
createsuperuser "$@"
else
exec "$@"
fi

View File

@@ -66,23 +66,9 @@
</div> </div>
<div class="row h-100 mb-4"> <div class="row h-100 mb-4">
<div class="col-lg-3 pt-6 px-4 d-none d-lg-block" > <div class="col-lg-3 pt-6 px-4 d-none d-lg-block" >
<ul class="list-group mt-2"> {% block left_sidebar %}
<li class="list-group-item bg-primary text-white"> {{ tinywiki_sidebar }}
<span class="list-group-badge">TinyWiki Pages</span> {% enblock left_sidebar %}
</li>
<li class="list-group-item">
<a href="{% url "tinywiki:page" slug="tw-bbcode" %}">BBCode Guide</a>
</li>
<li class="list-group-item">
<a href="{% url "tinywiki:page" slug="tw-markdown" %}">Markdown Guide</a>
</li>
<li class="list-group-item">
<a href="{% url "tinywiki:page" slug="tw-license" %}">TinyWiki License</a>
</li>
<li class="list-group-item">
<a href="{% url "tinywiki:page" slug="tw-bootstrap-license" %}">Bootstrap License</a>
</li>
</ul>
</div> </div>
<div class="col-lg-6"> <div class="col-lg-6">
<main> <main>

View File

@@ -0,0 +1,10 @@
from django.http import HttpRequest
from django.utils.safestring import mark_safe
from .models import SidebarSection
def sidebar(request: HttpRequest):
sections = [
section.widget
for section in SidebarSection.objects.filter(is_visible=True).order_by('-priority'):
]
return {'tinywiki_sidebar': mark_safe("\n".join(sections))}

View File

@@ -7,10 +7,12 @@ class Migration(migrations.Migration):
('tinywiki', '0001_initial'), ('tinywiki', '0001_initial'),
] ]
@staticmethod
def init_tinywiki_user(apps, schema_editor): def init_tinywiki_user(apps, schema_editor):
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
user = get_user_model().objects.create_user(**settings.TINYWIKI_USER_CONFIG) get_user_model().objects.create_user(**settings.TINYWIKI_USER_CONFIG)
@staticmethod
def init_tinywiki_groups_and_permissions(apps, schema_editor): def init_tinywiki_groups_and_permissions(apps, schema_editor):
from ..models import Page from ..models import Page
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
@@ -128,4 +130,3 @@ class Migration(migrations.Migration):
migrations.RunPython(init_builtin_images), migrations.RunPython(init_builtin_images),
migrations.RunPython(init_builtin_pages), migrations.RunPython(init_builtin_pages),
] ]

View File

@@ -0,0 +1,41 @@
# Generated by Django 5.2.9 on 2025-12-23 15:57
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tinywiki', '0002_initial_data'),
]
operations = [
migrations.CreateModel(
name='SidebarSection',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='title')),
('priority', models.PositiveIntegerField(verbose_name='priority')),
('is_visible', models.BooleanField(default=True, verbose_name='is visible')),
],
),
migrations.AlterField(
model_name='page',
name='status_data',
field=models.CharField(choices=[('in_progress', 'in progress'), ('draft', 'draft'), ('published', 'published')], default='in_progress', max_length=15, verbose_name='status'),
),
migrations.CreateModel(
name='SidebarEntry',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='Title')),
('is_visible', models.BooleanField(default=True, verbose_name='is visible')),
('priority', models.PositiveIntegerField(verbose_name='Priority')),
('wiki_slug', models.CharField(blank=True, max_length=255, null=True, verbose_name='Wiki slug')),
('url', models.CharField(blank=True, max_length=512, null=True, verbose_name='Link URL')),
('widget', models.TextField(blank=True, null=True, verbose_name='Widget')),
('section', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='tinywiki.sidebarsection', verbose_name='Section')),
],
),
]

View File

@@ -0,0 +1,92 @@
# Generated by Django 5.2.9 on 2025-12-23 15:59
from django.db import migrations
class Migration(migrations.Migration):
@staticmethod
def create_tinywiki_sections(apps, schema_editor):
from tinywiki.models import SidebarSection, SidebarEntry
data = [
{
"title": "TinyWiki Pages",
"is_visible": True,
"priority": 1000,
"items": [
{
"title": "BBCode Guide",
"is_visible": True,
"priority": 100,
"wiki_slug": "tw-bbcode",
},
{
"title": "Markdown Guide",
"is_visible": True,
"priority": 90,
"wiki_slug": "tw-markdown"
},
{
"title": "TinyWiki License",
"is_visible": True,
"priority": 80,
"wiki_slug": "tw-license"
},
{
"title": "Bootstrap License",
"is_visible": True,
"priority": 70,
"wiki_slug": "tw-bootstrap-license"
},
]
},
{
"title": "TinyWiki Seiten",
"is_visible": False,
"priority": 1000,
"items": [
{
"title": "BBCode Anleitung",
"is_visible": True,
"priority": 100,
"wiki_slug": "tw-de-bbcode",
},
{
"title": "Markdown Anleitung",
"is_visible": True,
"priority": 90,
"wiki_slug": "tw-de-markdown"
},
{
"title": "TinyWiki Lizenz (EN)",
"is_visible": True,
"priority": 80,
"wiki_slug": "tw-license"
},
{
"title": "Bootstrap License (EN)",
"is_visible": True,
"priority": 70,
"wiki_slug": "tw-bootstrap-license"
},
]
},
]
for sect_spec in data:
if 'items' in sect_spec:
items = sect_spec.pop('items')
else:
items = []
section = SidebarSection.objects.create(**sect_spec)
for item_spec in items:
SidebarEntry.objects.create(section=section, **item_spec)
dependencies = [
('tinywiki', '0003_sidebarsection_alter_page_status_data_sidebarentry'),
]
operations = [
]

View File

@@ -1,12 +1,17 @@
from tabnanny import verbose
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils.safestring import mark_safe, SafeText from django.utils.safestring import mark_safe, SafeText
from django.contrib.auth import get_user_model from django.utils.html import escape
from django.urls import reverse
from tinywiki.enums import WIKI_CONTENT_TYPES, WIKI_PAGE_STATUS, WikiContentType, WikiPageStatus from tinywiki.enums import (
WIKI_CONTENT_TYPES,
WIKI_PAGE_STATUS,
WikiContentType,
WikiPageStatus,
)
import markdown import markdown
import bbcode as _bbcode import bbcode as _bbcode
@@ -16,6 +21,7 @@ from .parser import parse_bbcode
from . import settings from . import settings
import tinywiki import tinywiki
def get_tinywiki_default_user(): def get_tinywiki_default_user():
UserModel = get_user_model() UserModel = get_user_model()
try: try:
@@ -24,6 +30,7 @@ def get_tinywiki_default_user():
user = UserModel.objects.filter(is_superuser=True).order_by('pk')[0] user = UserModel.objects.filter(is_superuser=True).order_by('pk')[0]
return user return user
class Page(models.Model): class Page(models.Model):
slug = models.SlugField(_("slug"), slug = models.SlugField(_("slug"),
max_length=255, max_length=255,
@@ -134,3 +141,77 @@ class Image(models.Model):
def description_html_safe(self) -> SafeText: def description_html_safe(self) -> SafeText:
return mark_safe(self.description_html) return mark_safe(self.description_html)
class SidebarSection(models.Model):
title = models.CharField(_("title"),
max_length=255,
null=False,
blank=False)
priority = models.PositiveIntegerField(_("priority"))
is_visible = models.BooleanField(_("is visible"),
default=True)
@property
def widget(self) -> SafeText:
if settings.USE_BOOTSTRAP:
lines = [
"<ul class=\"list-group mt-2\">",
f"<li class=\"list-group-item bg-primary text-white\">{escape(self.title)}</li>", # noqa: E501
*[item.item for item in self.items.filter(is_visible=True).order_by('-priority')], # noqa: E501
"</ul>"
]
else:
lines = [
"<ul class=\"sidebar-section\">",
f"<li class=\"sidebar-title\">{escape(self.title)}</li>",
*[item.item for item in self.items.filter(is_visible=True).order_by('-priority')], # noqa: E501
"</ul>"
]
return mark_safe("\n".join(lines))
class SidebarEntry(models.Model):
section = models.ForeignKey(SidebarSection,
on_delete=models.CASCADE,
verbose_name=_("Section"),
related_name="items")
title = models.CharField(_("Title"),
max_length=255,
null=False,
blank=False)
is_visible = models.BooleanField(_("is visible"),
default=True)
priority = models.PositiveIntegerField(_("Priority"))
wiki_slug = models.CharField(_("Wiki slug"),
max_length=255,
null=True,
blank=True)
url = models.CharField(_("Link URL"),
max_length=512,
null=True,
blank=True)
widget = models.TextField(_("Widget"),
blank=True,
null=True)
@property
def link(self) -> SafeText:
if self.wiki_slug:
return mark_safe(f"<a href=\"{reverse('tinywiki:page', kwargs={'slug': self.wiki_slug})}\">{escape(self.title)}</a>") # noqa: E501
elif self.url:
return mark_safe(f"<a href=\"{self.url}\">{escape(self.title)}</a>") # noqa E501
return mark_safe(f"<a href=\"#\">{escape(self.title)}</a>") # noqa E501
@property
def item(self) -> SafeText:
if settings.USE_BOOTSTRAP:
if self.widget:
return mark_safe(f"<li class=\"list-group-item\">{self.widget}</li>") # noqa: E501
else:
return mark_safe(f"<li class=\"list-group-item\">{self.link}</li>") # noqa: E501
if self.widget:
return mark_safe(f"<li class=\"sidebar-item\">{self.widget}</li>")
else:
return mark_safe(f"<li class=\"sidebar-item\">{self.link}</li>")

View File

@@ -334,6 +334,7 @@ def render_table_data(tag_name:str,value,options,parent,context):
class_attr = f"class=\"{" ".join(classes)}\"" if classes else "" class_attr = f"class=\"{" ".join(classes)}\"" if classes else ""
return f"<td {class_attr} {" ".join(extra_attributes)}>{value}</td>" return f"<td {class_attr} {" ".join(extra_attributes)}>{value}</td>"
def render_youtube_video(tag_name: str, value, options, parent, context): def render_youtube_video(tag_name: str, value, options, parent, context):
if tag_name not in options: if tag_name not in options:
return "" return ""
@@ -345,8 +346,11 @@ def render_youtube_video(tag_name: str, value, options, parent, context):
if settings.USE_BOOTSTRAP: if settings.USE_BOOTSTRAP:
styles = [] styles = []
classes = ["w-100"] classes = ["embed-responsive-item", "w-100"]
div_classes = ["my-1"] div_classes = [
"embed-responsive",
"my-1",
]
div_styles = [] div_styles = []
else: else:
styles = ["max-width:100%;"] styles = ["max-width:100%;"]
@@ -425,6 +429,8 @@ def render_youtube_video(tag_name: str, value, options, parent, context):
else: else:
div_style = "" div_style = ""
if settings.USE_BOOTSTRAP: if settings.USE_BOOTSTRAP:
return f'<div class=""{' '.join(div_classes)}" {div_style}><iframe class={" ".join(classes)}" src="https://www.youtube.com/embed/{options[tag_name]}" allowfullscreen></iframe></div>' return f"""<div class=""{' '.join(div_classes)}" {div_style}>
<iframe class={" ".join(classes)}" src="https://www.youtube.com/embed/{options[tag_name]}?rel=0" allowfullscreen></iframe>
</div>"""
else: else:
return f'<div {div_style}><iframe src="https://www.youtube.com/embed/{options[tag_name]}?rel=0" allowfullscreen></iframe></div>' return f'<div {div_style}><iframe src="https://www.youtube.com/embed/{options[tag_name]}?rel=0" allowfullscreen></iframe></div>'

View File

@@ -1,9 +1,9 @@
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.urls import reverse, reverse_lazy from django.urls import reverse
from django.http import HttpRequest, HttpResponse, Http404 from django.http import HttpRequest, HttpResponse, Http404
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from .. import settings from .. import settings
@@ -12,6 +12,8 @@ from ..models import Page
from .base import View,FormView from .base import View,FormView
from ..forms import PageForm,PageAdminForm,PageDeleteForm from ..forms import PageForm,PageAdminForm,PageDeleteForm
class PageView(View): class PageView(View):
template_name = "tinywiki/page/view.html" template_name = "tinywiki/page/view.html"
bs_template_name = "tinywiki/page/bs-view.html" bs_template_name = "tinywiki/page/bs-view.html"
@@ -39,7 +41,8 @@ class PageView(View):
or (user.pk == page.author.pk and user.has_perm('page.tinywiki-edit'))): or (user.pk == page.author.pk and user.has_perm('page.tinywiki-edit'))):
can_edit_page = True can_edit_page = True
if (user.has_perm('page.tinywiki-delete-all') if (user.has_perm('page.tinywiki-delete-all')
or (user.pk == page.author.pk and user.has_perm('page.tinywiki-delete'))): or (user.pk == page.author.pk
and user.has_perm('page.tinywiki-delete'))):
can_delete_page = True can_delete_page = True
kwargs.update({'page':page, kwargs.update({'page':page,
@@ -48,7 +51,6 @@ class PageView(View):
'subtitle':page.title}) 'subtitle':page.title})
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def get(self, request: HttpRequest, slug: str) -> HttpResponse: def get(self, request: HttpRequest, slug: str) -> HttpResponse:
try: try:
page = Page.objects.get(slug=slug) page = Page.objects.get(slug=slug)
@@ -60,6 +62,7 @@ class PageView(View):
self.get_template_name(), self.get_template_name(),
self.get_context_data(page=page)) self.get_context_data(page=page))
class PageCreateView(LoginRequiredMixin, UserPassesTestMixin, FormView): class PageCreateView(LoginRequiredMixin, UserPassesTestMixin, FormView):
template_name = "tinywiki/page/create.html" template_name = "tinywiki/page/create.html"
bs_template_name = "tinywiki/page/bs-create.html" bs_template_name = "tinywiki/page/bs-create.html"