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.contrib.auth.context_processors.auth',
'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)
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
CSP_DEFAULT_SRC = [
"'self'",
"https://youtube.com",
]
_secret_key = LOCAL_DIR / "secret_key.py"
if _secret_key.is_file():
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)",
"bbcode (>=1.1.0,<2.0.0)",
"markdown (>=3.9,<4.0)",
"django-csp (>=4.0,<5.0)",
]
[tool.poetry]

View File

@@ -1,46 +1,84 @@
#!/bin/bash
echo "Migrating database ..."
poetry run python manage.py migrate
if [ $? -ne 0 ]; then
echo "Unable to migrate database!" >&2
exit 5
fi
: ${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}
echo "Running collectstatic ..."
yes yes | poetry run python manage.py collectstatic
if [ $? -ne 0 ]; then
echo "Unable to collect static files!" >&2
exit 5
fi
if [ -z "$DEBUG" -o $DEBUG != "1" ]; then
echo "Starting UWSGI server ..."
venv="$(poetry env info | head -n 6 | grep Path | awk '{print $2}')"
poetry run uwsgi \
--chdir /app \
--module django_project.wsgi:application \
--master --pidfile /data/run/uwsgi.pid \
--http-socket '0.0.0.0:8000' \
--socket /data/run/uwsgi.sock \
--processes 5 \
--harakiri 60 \
--max-requests 5000 \
--vacuum \
--venv "$venv" \
--home "$venv"
rc=$?
if [ $rc -ne 0 ]; then
echo "UWSGI Server not started" >&2
exit $rc
migrate_db() {
echo "Migrating database ..."
poetry run python manage.py migrate
if [ $? -ne 0 ]; then
echo "Unable to migrate database!" >&2
exit 5
fi
}
collectstatic() {
echo "Running collectstatic ..."
yes yes | poetry run python manage.py collectstatic
if [ $? -ne 0 ]; then
echo "Unable to collect static files!" >&2
exit 5
fi
}
createsuperuser() {
poetry run python manage.py createsuperuser "$@"
}
if [ $# -eq 0 -o "$1" = "start" ]; then
if [ -z "$DEBUG" -o $DEBUG != "1" ]; then
echo "Starting UWSGI server ..."
venv="$(poetry env info | head -n 6 | grep Path | awk '{print $2}')"
poetry run uwsgi \
--chdir /app \
--module django_project.wsgi:application \
--master --pidfile /data/run/uwsgi.pid \
--http-socket "$HTTP_SOCKET" \
--socket "$UWSGI_SOCKET" \
--processes $UWSGI_PROCESSES \
--harakiri 60 \
--max-requests $UWSGI_MAX_REQUESTS \
--buffer-size $UWSGI_BUFFER_SIZE \
--vacuum \
--venv "$venv" \
--home "$venv"
rc=$?
if [ $rc -ne 0 ]; then
echo "UWSGI Server not started" >&2
exit $rc
fi
else
echo "Starting development server ..."
poetry run python manage.py runserver $HTTP_SOCKET
rc=$?
if [ $rc -ne 0 ]; then
echo "Developemnt server was not started!" >&2
exit $rc
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
echo "Starting development server ..."
poetry run python manage.py runserver 0.0.0.0:8000
rc=$?
if [ $rc -ne 0 ]; then
echo "Developemnt server was not started!" >&2
exit $rc
fi
exec "$@"
fi

View File

@@ -66,23 +66,9 @@
</div>
<div class="row h-100 mb-4">
<div class="col-lg-3 pt-6 px-4 d-none d-lg-block" >
<ul class="list-group mt-2">
<li class="list-group-item bg-primary text-white">
<span class="list-group-badge">TinyWiki Pages</span>
</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>
{% block left_sidebar %}
{{ tinywiki_sidebar }}
{% enblock left_sidebar %}
</div>
<div class="col-lg-6">
<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,13 +7,15 @@ class Migration(migrations.Migration):
('tinywiki', '0001_initial'),
]
def init_tinywiki_user(apps,schema_editor):
@staticmethod
def init_tinywiki_user(apps, schema_editor):
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)
def init_tinywiki_groups_and_permissions(apps,schema_editor):
@staticmethod
def init_tinywiki_groups_and_permissions(apps, schema_editor):
from ..models import Page
from django.contrib.auth.models import Group,Permission
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
PERMISSIONS = [
@@ -128,4 +130,3 @@ class Migration(migrations.Migration):
migrations.RunPython(init_builtin_images),
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.utils.translation import gettext_lazy as _
from django.conf import settings
from django.contrib.auth import get_user_model
from django.utils.safestring import mark_safe,SafeText
from django.contrib.auth import get_user_model
from tinywiki.enums import WIKI_CONTENT_TYPES, WIKI_PAGE_STATUS, WikiContentType, WikiPageStatus
from django.utils.safestring import mark_safe, SafeText
from django.utils.html import escape
from django.urls import reverse
from tinywiki.enums import (
WIKI_CONTENT_TYPES,
WIKI_PAGE_STATUS,
WikiContentType,
WikiPageStatus,
)
import markdown
import bbcode as _bbcode
@@ -16,6 +21,7 @@ from .parser import parse_bbcode
from . import settings
import tinywiki
def get_tinywiki_default_user():
UserModel = get_user_model()
try:
@@ -24,6 +30,7 @@ def get_tinywiki_default_user():
user = UserModel.objects.filter(is_superuser=True).order_by('pk')[0]
return user
class Page(models.Model):
slug = models.SlugField(_("slug"),
max_length=255,
@@ -74,10 +81,10 @@ class Page(models.Model):
def content_type(self)->WikiContentType:
return WikiContentType.from_string(self.content_type_data)
@content_type.setter
def content_type(self,content_type:str|WikiContentType):
if isinstance(content_type,str):
def content_type(self, content_type: str | WikiContentType):
if isinstance(content_type, str):
self.content_type_data = WikiContentType.from_string(content_type).value
elif isinstance(content_type,WikiContentType):
elif isinstance(content_type, WikiContentType):
self.content_type_data = content_type.value
else:
raise TypeError("content_type")
@@ -86,16 +93,16 @@ class Page(models.Model):
def status(self)->WikiPageStatus:
return WikiPageStatus.from_string(self.status_data)
@status.setter
def status(self,status:str|WikiPageStatus):
if isinstance(status,str):
def status(self, status: str | WikiPageStatus):
if isinstance(status, str):
self.status_data = WikiPageStatus.from_string(status).value
elif isinstance(status,WikiPageStatus):
elif isinstance(status, WikiPageStatus):
self.status_data = status.value
else:
raise TypeError("status")
@property
def html_content(self)->SafeText|str:
def html_content(self) -> SafeText | str:
if self.content_type == WikiContentType.MARKDOWN:
return mark_safe(markdown.markdown(self.content))
elif self.content_type == WikiContentType.BBCODE:
@@ -127,10 +134,84 @@ class Image(models.Model):
auto_now_add=True)
@property
def description_html(self)->str:
def description_html(self) -> str:
return _bbcode.render_html(self.description)
@property
def description_html_safe(self)->SafeText:
def description_html_safe(self) -> SafeText:
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 ""
return f"<td {class_attr} {" ".join(extra_attributes)}>{value}</td>"
def render_youtube_video(tag_name: str, value, options, parent, context):
if tag_name not in options:
return ""
@@ -345,8 +346,11 @@ def render_youtube_video(tag_name: str, value, options, parent, context):
if settings.USE_BOOTSTRAP:
styles = []
classes = ["w-100"]
div_classes = ["my-1"]
classes = ["embed-responsive-item", "w-100"]
div_classes = [
"embed-responsive",
"my-1",
]
div_styles = []
else:
styles = ["max-width:100%;"]
@@ -385,9 +389,9 @@ def render_youtube_video(tag_name: str, value, options, parent, context):
styles.append(f"height:{_h};")
else:
if _h.endswith('%'):
_h= _h[:-1]
_h = _h[:-1]
if _h.isdigit():
_h=int(_w)
_h = int(_w)
if _h > 100:
_h = 100
if settings.USE_BOOTSTRAP:
@@ -405,15 +409,15 @@ def render_youtube_video(tag_name: str, value, options, parent, context):
if "position" in options:
pos = options['position']
if settings.USE_BOOTSTRAP:
if pos == "left" or pos=="start":
div_classes += ["float-start","me-2"]
if pos == "left" or pos == "start":
div_classes += ["float-start", "me-2"]
#classes += ["float-start","me-2"]
elif pos == "right" or pos == "end":
div_classes += ["float-end","ms-2"]
#classes += ["float-end","ms-2"]
div_classes += ["float-end", "ms-2"]
#classes += ["float-end", "ms-2"]
elif pos == "center":
div_classes += ["mx-auto","d-block"]
#classes += ["mx-auto","d-block"]
div_classes += ["mx-auto", "d-block"]
#classes += ["mx-auto", "d-block"]
if styles:
style = f"style=\"{"".join(styles)}\""
@@ -425,6 +429,8 @@ def render_youtube_video(tag_name: str, value, options, parent, context):
else:
div_style = ""
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:
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.urls import reverse, reverse_lazy
from django.http import HttpRequest,HttpResponse,Http404
from django.contrib.auth.mixins import LoginRequiredMixin,UserPassesTestMixin
from django.core.exceptions import PermissionDenied
from django.shortcuts import render, get_object_or_404, redirect
from django.urls import reverse
from django.http import HttpRequest, HttpResponse, Http404
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.exceptions import PermissionDenied
from django.utils.translation import gettext as _
from .. import settings
@@ -12,6 +12,8 @@ from ..models import Page
from .base import View,FormView
from ..forms import PageForm,PageAdminForm,PageDeleteForm
class PageView(View):
template_name = "tinywiki/page/view.html"
bs_template_name = "tinywiki/page/bs-view.html"
@@ -22,7 +24,7 @@ class PageView(View):
return cls.bs_template_name
return cls.template_name
def get_context_data(self,page,**kwargs):
def get_context_data(self, page, **kwargs):
can_edit_page = False
can_delete_page = False
user = self.request.user
@@ -39,7 +41,8 @@ class PageView(View):
or (user.pk == page.author.pk and user.has_perm('page.tinywiki-edit'))):
can_edit_page = True
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
kwargs.update({'page':page,
@@ -48,8 +51,7 @@ class PageView(View):
'subtitle':page.title})
return super().get_context_data(**kwargs)
def get(self,request:HttpRequest,slug:str)->HttpResponse:
def get(self, request: HttpRequest, slug: str) -> HttpResponse:
try:
page = Page.objects.get(slug=slug)
except Page.DoesNotExist:
@@ -60,7 +62,8 @@ class PageView(View):
self.get_template_name(),
self.get_context_data(page=page))
class PageCreateView(LoginRequiredMixin,UserPassesTestMixin,FormView):
class PageCreateView(LoginRequiredMixin, UserPassesTestMixin, FormView):
template_name = "tinywiki/page/create.html"
bs_template_name = "tinywiki/page/bs-create.html"
form_class = PageForm
@@ -77,12 +80,12 @@ class PageCreateView(LoginRequiredMixin,UserPassesTestMixin,FormView):
return self.bs_template_name
return self.template_name
def get(self,request):
def get(self, request):
return render(request,
self.get_template_name(),
self.get_context_data())
def get_context_data(self,**kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['create']=True
context['slug'] = self.request.GET.get('slug',None)
@@ -130,12 +133,12 @@ class PageCreateView(LoginRequiredMixin,UserPassesTestMixin,FormView):
content_extra=content_extra,
))
def form_valid(self,form):
def form_valid(self, form):
user = self.request.user
instance = form.save(commit=False)
if (instance.slug.startswith('tw-')
and not user.is_staff and
not user.has_perm('page.tinywiki-create-system')):
and not user.is_staff and
not user.has_perm('page.tinywiki-create-system')):
return render(self.request,
self.get_template_name(),
self.get_context_data(