2025.10.06-03:08:05

This commit is contained in:
2025-10-06 03:08:05 +02:00
parent 38a85cb9d5
commit e08b03bb42
57 changed files with 3222 additions and 157 deletions

View File

@@ -0,0 +1,91 @@
{% extends "account/base_manage_email.html" %}
{% load static allauth i18n widget_tweaks %}
{% block head_title %}
{% trans "Email Addresses" %}
{% endblock head_title %}
{% block content %}
{% element h1 %}
{% trans "Email Addresses" %}
{% endelement %}
{% if emailaddresses %}
{% element p %}
{% trans 'The following email addresses are associated with your account:' %}
{% endelement %}
{% url 'account_email' as email_url %}
{% element form form=form action=email_url method="post" tags="email,list" %}
{% slot body %}
{% csrf_token %}
{% for radio in emailaddress_radios %}
{% with emailaddress=radio.emailaddress %}
{% element field type="radio" checked=radio.checked name="email" value=emailaddress.email id=radio.id %}
{% slot label %}
{{ emailaddress.email }}
{% if emailaddress.verified %}
{% element badge tags="success,email,verified" %}
{% translate "Verified" %}
{% endelement %}
{% else %}
{% element badge tags="warning,email,unverified" %}
{% translate "Unverified" %}
{% endelement %}
{% endif %}
{% if emailaddress.primary %}
{% element badge tags="email,primary" %}
{% translate "Primary" %}
{% endelement %}
{% endif %}
{% endslot %}
{% endelement %}
{% endwith %}
{% endfor %}
{% endslot %}
{% slot actions %}
<div class="text-end">
{% element button type="submit" class="btn btn-primary mb-3" name="action_primary" %}
{% trans 'Make Primary' %}
{% endelement %}
{% element button tags="secondary" class="btn btn-secondary mb-3" type="submit" name="action_send" %}
{% trans 'Re-send Verification' %}
{% endelement %}
{% element button class="btn btn-danger mb-3" tags="danger,delete" type="submit" name="action_remove" %}
{% trans 'Remove' %}
{% endelement %}
</div>
{% endslot %}
{% endelement %}
{% else %}
{% include "account/snippets/warn_no_email.html" %}
{% endif %}
{% if can_add_email %}
{% element h2 %}
{% trans "Add Email Address" %}
{% endelement %}
{% url 'account_email' as action_url %}
{% element form form=form method="post" action=action_url tags="email,add" %}
{% slot body %}
{% csrf_token %}
<div class="form-floating form-floating-sm mb-3">
{% render_field form.email class="form-control" placeholder="Neue Email-Adresse" %}
<label for="id_email">Neue Email-Adresse</label>
</div>
{% endslot %}
{% slot actions %}
<div class="text-end">
{% element button name="action_add" class="btn btn-success" type="submit" %}
{% trans "Add Email" %}
{% endelement %}
</div>
{% endslot %}
{% endelement %}
{% endif %}
{% endblock content %}
{% block extra_body %}
<script src="{% static 'account/js/account.js' %}"></script>
<script src="{% static 'account/js/onload.js' %}"></script>
<script data-allauth-onload="allauth.account.forms.manageEmailForm" type="application/json">{
"i18n": {"confirmDelete": "{% trans 'Do you really want to remove the selected email address?' %}"}
}
</script>
{% endblock extra_body %}

View File

@@ -0,0 +1,68 @@
{% extends "account/base_manage_email.html" %}
{% load i18n %}
{% load allauth %}
{% block head_title %}
{% trans "Email Address" %}
{% endblock head_title %}
{% block content %}
{% element h1 %}
{% trans "Email Address" %}
{% endelement %}
{% if not emailaddresses %}
{% include "account/snippets/warn_no_email.html" %}
{% endif %}
{% url 'account_email' as action_url %}
{% element form method="post" action=action_url %}
{% slot body %}
{% csrf_token %}
{% if current_emailaddress %}
{% element field id="current_email" disabled=True type="email" value=current_emailaddress.email %}
{% slot label %}
{% translate "Current email" %}:
{% endslot %}
{% endelement %}
{% endif %}
{% if new_emailaddress %}
{% element field id="new_email" value=new_emailaddress.email disabled=True type="email" %}
{% slot label %}
{% if not current_emailaddress %}
{% translate "Current email" %}:
{% else %}
{% translate "Changing to" %}:
{% endif %}
{% endslot %}
{% slot help_text %}
{% blocktranslate %}Your email address is still pending verification.{% endblocktranslate %}
{% element button class="btn btn-secondary" form="pending-email" type="submit" name="action_send" tags="minor,secondary" %}
{% trans 'Re-send Verification' %}
{% endelement %}
{% if current_emailaddress %}
{% element button class="btn btn-danger" form="pending-email" type="submit" name="action_remove" tags="danger,minor" %}
{% trans 'Cancel Change' %}
{% endelement %}
{% endif %}
{% endslot %}
{% endelement %}
{% endif %}
{% element field id=form.email.auto_id name="email" value=form.email.value errors=form.email.errors type="email" %}
{% slot label %}
{% translate "Change to" %}:
{% endslot %}
{% endelement %}
{% endslot %}
{% slot actions %}
{% element button class="btn btn-primary" name="action_add" type="submit" %}
{% trans "Change Email" %}
{% endelement %}
{% endslot %}
{% endelement %}
{% if new_emailaddress %}
<form style="display: none"
id="pending-email"
method="post"
action="{% url 'account_email' %}">
{% csrf_token %}
<input type="hidden" name="email" value="{{ new_emailaddress.email }}">
</form>
{% endif %}
{% endblock content %}

View File

@@ -0,0 +1,41 @@
{% extends "account/base_entrance.html" %}
{% load i18n %}
{% load account %}
{% load allauth %}
{% block head_title %}
{% trans "Confirm Email Address" %}
{% endblock head_title %}
{% block content %}
{% element h1 %}
{% trans "Confirm Email Address" %}
{% endelement %}
{% if confirmation %}
{% user_display confirmation.email_address.user as user_display %}
{% if can_confirm %}
{% element p %}
{% blocktrans with confirmation.email_address.email as email %}Please confirm that <a href="mailto:{{ email }}">{{ email }}</a> is an email address for user {{ user_display }}.{% endblocktrans %}
{% endelement %}
{% url 'account_confirm_email' confirmation.key as action_url %}
{% element form method="post" action=action_url %}
{% slot actions %}
{% csrf_token %}
{{ redirect_field }}
<div class="text-end">
{% element button class="btn btn-success" type="submit" %}
{% trans 'Confirm' %}
{% endelement %}
</div>
{% endslot %}
{% endelement %}
{% else %}
{% element p %}
{% blocktrans %}Unable to confirm {{ email }} because it is already confirmed by a different account.{% endblocktrans %}
{% endelement %}
{% endif %}
{% else %}
{% url 'account_email' as email_url %}
{% element p %}
{% blocktrans %}This email confirmation link expired or is invalid. Please <a href="{{ email_url }}">issue a new email confirmation request</a>.{% endblocktrans %}
{% endelement %}
{% endif %}
{% endblock content %}

View File

@@ -0,0 +1,49 @@
{% extends 'base.html' %}
{% load i18n widget_tweaks allauth account socialaccount %}
{% block head_title %}cmoser.eu - {% translate 'Log in' %}{% endblock head_title %}
{% block content %}
<main class="card rounded mx-auto p-4" style="width:30rem;">
<div class="card-body">
<div class="d-flex flex-column">
<div class="flex flex-col text-center mb-4">
<h1 class="h3">Melde dich bei deinem Account an</h1>
<form method="POST" class="mb-4">
{% csrf_token %}
{% if form.errors %}
{% for field, errors in form.errors.items %}
{% for error in errors %}
<div class="p-2 my-2 text-sm text-red-700 bg-red-50 rounded-md border-red-300 border">
{{ error }}
</div>
{% endfor %}
{% endfor %}
{% endif %}
<div class="form-floating mb-3">
{% render_field form.login class="form-control" placeholder="Emailadresse" %}
<label for="{{ form.login.id_for_label }}">Emailaddresse</label>
</div>
<div class="form-floating mb-3">
{% render_field form.password class="form-control" %}
<label for="{{ form.password.id_for_label }}">Passwort</label>
</div>
<div>
{% render_field form.remember class="input-checked" %}
<label class="input-checked-label" for="{{ form.remember.id_for_label }}">{{ form.remember.label }}</label>
</div>
<div class="text-end">
<button type="submit" class="btn btn-success">Sign in</button>
</div>
</form>
</div>
<hr>
<h2 class="h4 text-center">Oder nutze deinen Socialaccount</h3>
{% if SOCIALACCOUNT_ENABLED %}
{% include 'socialaccount/snippets/login.html' with page_layout="entrance" %}
{% endif %}
</div>
</main>
{% endblock content %}

View File

@@ -0,0 +1,39 @@
{% extends "account/base_manage_password.html" %}
{% load allauth i18n widget_tweaks %}
{% block head_title %}
{% trans "Change Password" %}
{% endblock head_title %}
{% block content %}
{% element h1 %}
{% trans "Change Password" %}
{% endelement %}
{% url 'account_change_password' as action_url %}
{% element form form=form method="post" action=action_url %}
{% slot body %}
{% csrf_token %}
{#{{ redirect_field }}#}
{# {% element fields form=form %}{% endelement %} #}
<div class="form-floating form-floating-sm mb-3">
{% render_field form.oldpassword placeholder="Aktuelles Passwort" class="form-control" %}
<label for="{{ id_oldpassword }}">Aktuelles Passwort</label>
</div>
<div class="form-floating form-floating-sm mb-3">
{% render_field form.password1 class="form-control" placehoder="Neues Passwort" autocomplete="new-password" required="" %}
<label for="{{ id_password1 }}">Neues Passwort</label>
</div>
<div class="form-floating form-flaoting-sm mb-3">
{% render_field form.password2 class="form-control" placehoder="Neues Passwort" autocomplete="new-password" required="" %}
<label for="{{ id_password2 }}">Neues Passwort</label>
</div>
{% endslot %}
{% slot actions %}
<div class="text-end">
<a class="me-2" href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
{% element button class="btn btn-primary" type="submit" %}
{% trans "Change Password" %}
{% endelement %}
</div>
{% endslot %}
{% endelement %}
{% endblock content %}

View File

@@ -0,0 +1,38 @@
{% extends "account/base_entrance.html" %}
{% load i18n allauth account widget_tweaks %}
{% block head_title %}
{% trans "Password Reset" %}
{% endblock head_title %}
{% block content %}
{% element h1 %}
{% trans "Password Reset" %}
{% endelement %}
{% if user.is_authenticated %}
{% include "account/snippets/already_logged_in.html" %}
{% endif %}
{% element p %}
{% trans "Forgotten your password? Enter your email address below, and we'll send you an email allowing you to reset it." %}
{% endelement %}
{% url 'account_reset_password' as reset_url %}
{% element form form=form method="post" action=reset_url %}
{% slot body %}
{% csrf_token %}
{{ redirect_field }}
<div class="form-floating form-floating-sm">
{% render_field form.email name="email" autocomplete="email" placeholder="E-Mail-Adresse" max-length="320" required="" class="form-control mb-3" %}
<label for="id_email">E-Mail-Adresse</label>
</div>
{% endslot %}
{% slot actions %}
<div class="text-end mb-3">
{% element button type="submit" class="btn btn-primary" %}
{% trans 'Reset My Password' %}
{% endelement %}
</div>
{% endslot %}
{% endelement %}
{% element p %}
{% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}
{% endelement %}
{% endblock content %}

View File

@@ -0,0 +1,13 @@
{% load allauth %}
{% comment %} djlint:off {% endcomment %}
<{% if attrs.href %}a href="{{ attrs.href }}"{% else %}button{% endif %}
{% if attrs.form %}form="{{ attrs.form }}"{% endif %}
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
{% if attrs.value %}value="{{ attrs.value }}"{% endif %}
{% if attrs.type %}type="{{ attrs.type }}"{% endif %}
{% if attrs.class %}class="{{ attrs.class }}"{% endif %}
>
{% slot %}
{% endslot %}
</{% if attrs.href %}a{% else %}button{% endif %}>

View File

@@ -0,0 +1,21 @@
{% load static %}
{% if not attrs.name|lower == "openid" %}
<li class="list-group-item">
<a title="{{ attrs.name }}" href="{{ attrs.href }}" class="icon-link">
{% if attrs.name|lower == "steam" %}
<svg class="bi">
<use xlink:href="{% static "img/bootstrap-icons.svg" %}#steam"></use>
</svg>
{% elif attrs.name|lower == "github" %}
<svg class="bi">
<use xlink:href="{% static "img/bootstrap-icons.svg" %}#github"></use>
</svg>
{% elif attrs.name|lower == "google" %}
<svg class="bi">
<use xlink:href="{% static "img/bootstrap-icons.svg" %}#google"></use>
</svg>
{% endif %}
{{ attrs.name }}
</a>
</li>
{% endif %}

View File

@@ -0,0 +1,5 @@
{% load allauth %}
<ul class="list-group">
{% slot %}
{% endslot %}
</ul>

View File

@@ -0,0 +1,2 @@
{% extends 'base.html' %}

View File

@@ -7,6 +7,9 @@
<title>TinyWiki</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
{% block extra_css %}{% endblock %}
<style>
{% block style %}{% endblock %}
</style>
<script>
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'));
</script>
@@ -15,8 +18,8 @@
<body class="min-vh-100 d-flex flex-column">
<div class="container-fluid">
<div class="row bg-info px-2">
<div clas="col-md-12 text-white">
<div class="row bg-primary pt-2">
<div clas="col-md-12 text-white ">
{% if brand_logo %}
<img class="mb2 me-2" width="46" height="46" src="{{ brand_logo }}">
{% else %}
@@ -26,7 +29,7 @@
{% endif %}
<span class="display-6 font-weight-bold text-white me-2">{{ brand_name }}</span>
{% if subtitle %}
<span class="h2">{{ subtitle }}</h2>
<span class="h2 text-truncate-sm">{{ subtitle }}</h2>
{% endif %}
</div>
</div>
@@ -61,8 +64,25 @@
</ul>
</div>
</div>
<div class="row h-100">
<div class="col-lg-3">
<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>
</div>
<div class="col-lg-6">
<main>
@@ -83,6 +103,7 @@
<a class="text-white" href="{% url 'tinywiki:page' 'tw-license' %}"> &copy; 2025</a>
</span>
</footer>
{% block extra_body %}{% endblock extra_body %}
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.min.js"></script>
{% block extra_scripts %}{% endblock extra_scripts %}

View File

@@ -0,0 +1,55 @@
{% extends 'base.html' %}
{# {% extends "socialaccount/base_manage.html" %} #}
{% load i18n %}
{% load allauth %}
{% block head_title %}
{% trans "Account Connections" %}
{% endblock head_title %}
{% block content %}
{% element h1 %}
{% trans "Account Connections" %}
{% endelement %}
{% if form.accounts %}
{% element p %}
{% blocktrans %}You can sign in to your account using any of the following third-party accounts:{% endblocktrans %}
{% endelement %}
{% url 'socialaccount_connections' as action_url %}
{% element form form=form method="post" action=action_url %}
{% slot body %}
{% csrf_token %}
{% for acc in form.fields.account.choices %}
{% with account=acc.0.instance.get_provider_account %}
{% setvar radio_id %}
id_account_{{ account.account.pk }}
{% endsetvar %}
{% setvar tags %}
socialaccount,{{ account.account.provider }}
{% endsetvar %}
{% element field id=radio_id type="radio" name="account" value=account.account.pk %}
{% slot label %}
{{ account }}
{% element badge tags=tags %}
{{ account.get_brand.name }}
{% endelement %}
{% endslot %}
{% endelement %}
{% endwith %}
{% endfor %}
{% endslot %}
{% slot actions %}
{% element button tags="delete,danger" class="btn btn-danger" type="submit" %}
{% trans 'Remove' %}
{% endelement %}
{% endslot %}
{% endelement %}
{% else %}
{% element p %}
{% trans 'You currently have no third-party accounts connected to this account.' %}
{% endelement %}
{% endif %}
{% element h2 %}
{% trans 'Add a Third-Party Account' %}
{% endelement %}
{% include "socialaccount/snippets/provider_list.html" with process="connect" %}
{% include "socialaccount/snippets/login_extra.html" %}
{% endblock content %}

View File

@@ -0,0 +1,18 @@
{% load allauth socialaccount %}
{% get_providers as socialaccount_providers %}
{% if socialaccount_providers %}
{% element provider_list %}
{% for provider in socialaccount_providers %}
{% if not provider.name == "OpenID" %}
{% for brand in provider.get_brands %}
{% provider_login_url provider openid=brand.openid_url process=process as href %}
{% element provider name=brand.name provider_id=provider.id href=href %}
{% endelement %}
{% endfor %}
{% endif %}
{% provider_login_url provider process=process scope=scope auth_params=auth_params as href %}
{% element provider name=provider.name provider_id=provider.id href=href %}
{% endelement %}
{% endfor %}
{% endelement %}
{% endif %}

View File

@@ -1,5 +1,6 @@
from django import forms
from .models import Page,Image
from django.utils.translation import gettext_lazy as _
class PageForm(forms.ModelForm):
class Meta:
@@ -12,6 +13,11 @@ class PageForm(forms.ModelForm):
'content',
]
class PageDeleteForm(forms.Form):
slug = forms.SlugField(allow_unicode=False,
required=False,
label=_("Slug"))
class PageAdminForm(forms.ModelForm):
class Meta:
model = Page
@@ -24,3 +30,25 @@ class PageAdminForm(forms.ModelForm):
'content',
]
class ImageUploadView(forms.ModelForm):
class Meta:
model = Image
fields = [
"image",
"alt",
"description",
]
class ImageEditView(forms.ModelForm):
class Meta:
model = Image
fields = [
"alt",
"description",
]
class ImageDeleteView(forms.Form):
slug = forms.SlugField(allow_unicode=False,
required=False)

View File

@@ -59,7 +59,7 @@ class Migration(migrations.Migration):
group.permissions.add(perm_mapping[perm])
def init_default_pages(apps,schema_editor)->None:
def init_builtin_pages(apps,schema_editor)->None:
from ..models import Page,Image
from ..enums import WikiContentType,WikiPageStatus
from pathlib import Path
@@ -85,14 +85,47 @@ class Migration(migrations.Migration):
content=content)
def init_builtin_images(apps,schema_edit):
from pathlib import Path
from ..models import Image
from django.core.files.images import ImageFile
import json
image_dir = Path(__file__).resolve().parent / "images"
images_json_file = image_dir/ "images.json"
if not images_json_file.is_file():
return
with open(images_json_file,"rt",encoding="utf-8") as ifile:
images_data = json.loads(ifile.read())
for slug,_spec in images_data.items():
spec = dict(_spec)
image_basename = spec['image']
image_filename = image_dir / spec["image"]
spec['slug'] = slug
del spec['image']
img = Image(**spec)
with open(image_filename,"rb") as ifile:
img_file = ImageFile(ifile,image_basename)
img.image.save(image_basename,img_file)
img.save()
def init_user_imges(apps,schema_edit)->None:
#TODO
pass
def init_user_pages(apps,schema_edit)->None:
from ..models import Page,Image
#TODO
pass
operations = [
migrations.RunPython(init_tinywiki_groups_and_permissions),
migrations.RunPython(init_tinywiki_user),
migrations.RunPython(init_default_pages),
migrations.RunPython(init_builtin_images),
migrations.RunPython(init_builtin_pages),
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

View File

@@ -0,0 +1,7 @@
{
"tw-image-01": {
"alt": "Foggy Mountain-tops in the dawn",
"image": "fabian-bachli-jQAe44MEIXU-unsplash.jpg",
"description": "Foto from [url=\"https://unsplash.com/de/@fabianbaechli?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash\"]Fabian Bächli[/url] on [url=\"https://unsplash.com/de/fotos/nebelige-berggipfel-in-der-morgendammerung-mit-sanften-wolken-jQAe44MEIXU?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash\"]Unsplash[/url]"
}
}

View File

@@ -0,0 +1,582 @@
[h1]BBCode used by TinyWiki[/h1]
[p]
BBCode uses tags to lay out your page. Tags written as opened by [code][tag][/code] and closed by [code][/tag][/code]. Standalone tags close automatically.
[/p]
[h2]Headers[/h2]
[p]
Headers are created by the [i]h[/i] tags. They range from [code][h1][/code] to [code][h6][/code]. You should start your page with a [code][h1][/code]-header. You shall only define one [code][h1][/code]-header in your page. It serves as the title displayed.
[/p]
[p]
You define headers as follows:
[/p]
[codeblock=bbcode][h1]Title header[/h1]
[h2]Header 2[/h2]
[h3]Header 3[/h3]
[h4]Header 4[/h4]
[h5]Header 5[/h5]
[h6]Header 6[/h6][/codeblock]
Which render as:
[/p]
[h1]Title header[/h1]
[h2]Header 2[/h2]
[h3]Header 3[/h3]
[h4]Header 4[/h4]
[h5]Header 5[/h5]
[h6]Header 6[/h6]
[hr]
[h2]Paragraphs[/h2]
[p]Paragraphs are created by the [code][p][/code] tag.[/p]
See the following code:
[codeblock=bbcode]
[hr]
[p]This is a paragrpah.[/p]
[p]And this is another one with a [b]encapsulated bold tag[/b].[/p]
[hr][/codeblock]
This renders as:[hr]
[p]This is a paragrpah.[/p]
[p]And this is another one with a [b]encapsulated bold tag[/b].[/p]
[hr]
[h2]Typography[/h2]
[p]
You can create [b]bold[/b] text with the [code][b][/code]-tag, [i]italic[/i] text with the [code][i][/code]-tag. To create text with [strong]strong emphasis[/strong], use the [code][strong][/code]-tag. [em]Emphasized text[/em] is created using the [code][em][/code]-tag. [u]Underlining text[/u] is done with the [code][u][/code]-tag, [s]Striked through[/s] text with the [code][s][/code]-tag. And to [mark]mark text[/mark] you can use the [code][mark][/code]-tag.
[/p]
[p]
The tags described in the previous paragraph are rendered as semantic-HTML and handled by screen-readers accordingly.
[/p]
[p]
If you want to show inline code, use the [code][code][/code]-tag. Code blocks can be can be created wtih the [code][codeblock][/code]-tag. You can also specify a language for codeblocks fields. this is done by using the [code][code=language][/code] option set to the codeblock-tag ([code][codeblock=bbcode][/code] is used to render the codeblock fields in this text).
[/p]
[p]
Line breaks are created with the [code][br][/code]-tag and horizontal rulers with the [code][hr][/code]-tag.
[/p]
[p]
An example:
[codeblock=bbcode][b]This text is bold.[/b][br]
[i]This text is italic.[/i][br]
[u]This is some underlined text.[/u][br]
[s]We can also strike through some text.[/s][br]
[strong]Here some text with strong emphasis.[/strong][br]
[em]Emphasized text can also be created.[/em][br]
[mark]And finally we mark some text.[/mark][br]
[code]finally_some_code()[/code]
[hr][/codeblock]
This renders as follows:
[/p]
[p]
[b]This text is bold.[/b][br]
[i]This text is italic.[/i][br]
[u]This is some underlined text.[/u][br]
[s]We can also strike through some text.[/s][br]
[strong]Here some text with strong emphasis.[/strong][br]
[em]Emphasized text can also be created.[/em][br]
[mark]And we mark some text.[/mark][br]
[code]finally_some_code()[/code]
[hr]
[/p]
[h2]Links[/h2]
[p]
Links are created using the [code][url][/code]-tag. You can create a link by using [code][url]www.example.com[/url[/code] to create a simple link with the same link description as the URL itself or you use the [code][url=https://www.example.com]Link description[/url][/code] style if you want the link description differ from the link itself.
[/p]
[p]
To link against a wiki-page, you can use the [code][wiki-url=page_slug][/code]-code or the [code][wiki][/code]-tag. The [code][wiki-url][/code] works nearly the same, is used to link against a page with a user-defined description. Whereas the [code][wiki=slug][/code]-tag, which is self-closing, is used to link against a wiki page and display the page's title.
[/p]
[p]Here an example:[/p]
[codeblock=bbcode][url]www.google.com[/url][br]
[url=https://duckduckgo.com]Another search engine[/url][br]
[wiki-url=tw-license]The license of Tinywiki[/wiki-url][br]
[wiki=tw-bbcode][br]
And a link to the wiki-homepage: [wiki][/codeblock]
[p]
[url]www.google.com[/url][br]
[url=https://duckduckgo.com]Another search engine[/url][br]
[wiki-url=tw-license]The license of Tinywiki[/wiki-url][br]
[wiki=tw-bbcode][br]
And a link to the wiki-homepage [wiki]
[/p]
[h2]Lists[/h2]
[p]
Unordered lists can be created using the [code][list][*] Item1 [*] Item2[/list][/code] tags. Alternatively, you can use the [code][ul][li]Item1[/li][li]Item2[/li][/ul][/code] notation.
[/p]
[p]
Ordered lists are created with the [code][ol][/code] tag. The notation is the same as the [code][ul][/code] tag.
[/p]
[p]
Here an example:
[codeblock=bbcode]
[hr]
An inline list:
[list]
[*] Item1
[*] Item2
[*] Item 3
[/list]
[hr]
An unordered list:
[ul]
[li] Item 1[/li]
[li] Item 2[/li]
[li]
[ul]
[li]Embedded 1[/li]
[li]Embedded 2[/li]
[/ul]
[/li]
[/ul]
[hr]
An ordered list:
[ol]
[li] Item 1[/li]
[li] Item 2[/li]
[li]
[ol]
[li] Embedded 1[/li]
[li]Embedded 2[/li]
[/ol]
[/li]
[/ol]
[/codeblock]
[p]
A list:
[list]
[*] Item1
[*] Item2
[*] Item 3
[/list]
[hr]
An unordered list:
[ul]
[li] Item 1[/li]
[li] Item 2[/li]
[li]
[ul]
[li]Embedded 1[/li]
[li]Embedded 2[/li]
[/ul]
[/li]
[/ul]
[hr]
An ordered list:
[ol]
[li] Item 1[/li]
[li] Item 2[/li]
[li]
[ol]
[li]Embedded 1[/li]
[li]Embedded 2[/li]
[/ol]
[/li]
[/ol]
[/p]
[h2]Tables[/h2]
[p]
Tables are created with the [code]table[/code] tag. They contain rows, created with the [code]table-row[/code] or [code][tr][/code] tag. Each row contain table headers or table data. Table headers are created with [code][table-header][/code] or [code][th][/code] tag, table data with the [code][table-data][/code] or [code][td][/code] tag.
[/p]
[codeblock=bbcode]
[table]
[table-row]
[table-header]Name[/table-header]
[table-header]Email[/table-header]
[table-header]Phone number[/table-header]
[table-row]
[table-row]
[table-data]John Doe[/table-data]
[table-data]johndoe@example.com[/table-data]
[table-data]345693887[/table-data]
[/table-row]
[table-row]
[table-data]Jane Doe[/table-data]
[table-data]janedoe@example.com[/table-data]
[table-data]685528874[/table-data]
[/table-row]
[table-row]
[table-data]Patrick Smith[/table-data]
[table-data]patricksmith@example.com[/table-data]
[table-data]5786255143[/table-data]
[/table-row]
[/table]
[/codeblock]
[table]
[table-row]
[table-header]Name[/table-header]
[table-header]Email[/table-header]
[table-header]Phone number[/table-header]
[table-row]
[table-row]
[table-data]John Doe[/table-data]
[table-data]johndoe@example.com[/table-data]
[table-data]345693887[/table-data]
[/table-row]
[table-row]
[table-data]Jane Doe[/table-data]
[table-data]janedoe@example.com[/table-data]
[table-data]685528874[/table-data]
[/table-row]
[table-row]
[table-data]Patrick Smith[/table-data]
[table-data]patricksmith@example.com[/table-data]
[table-data]5786255143[/table-data]
[/table-row]
[/table]
[h3]Bootstrap extensions[/h3]
[p][b]NOTE: The following tables might not render right when not using [url=https://getbootstrap.com]Bootstrap[/url]![/b][/p]
[p]
[p]
When using [i]Bootstrap[/i] you can style your tables. You can use [code]primary, secondary, info, warning, danger, success, light[/code] and [code]dark[/code] as an argument for the [code][table][/code] tag to give the table some color.
[/p]
[codeblock]
[table=primary]
[tr]
[th]Name[/th]
[th]Email[/th]
[th]Phone number[/th]
[/tr]
[tr]
[td]John Doe[/td]
[td]johndoe@example.com[/td]
[td]345693887[/td]
[/tr]
[tr]
[td]Jane Doe[/td]
[td]janedoe@example.com[/td]
[td]685528874[/td]
[/tr]
[tr]
[td]Patrick Smith[/td]
[td]patricksmith@example.com[/td]
[td]5786255143[/td]
[/tr]
[/table]
[/codeblock]
[table=primary]
[tr]
[th]Name[/th]
[th]Email[/th]
[th]Phone number[/th]
[/tr]
[tr]
[td]John Doe[/td]
[td]johndoe@example.com[/td]
[td]345693887[/td]
[/tr]
[tr]
[td]Jane Doe[/td]
[td]janedoe@example.com[/td]
[td]685528874[/td]
[/tr]
[tr]
[td]Patrick Smith[/td]
[td]patricksmith@example.com[/td]
[td]5786255143[/td]
[/tr]
[/table]
[p]When your wiki is powered by [i]Bootstrap[/i], you can also create bordered tables. If the bordered attribute is not one of [code]0, false, n, no[/code] or [code]off[/code], a bordered table is rendered. If the attribute is one of [code]primary, secondary, info, warning, danger, success, light[/code] or [code]dark[/code], the border colors are set accordingly.
[/p]
[codeblock]
[table bordered=1]
[tr]
[th]Name[/th]
[th]Email[/th]
[th]Phone number[/th]
[/tr]
[tr]
[td]John Doe[/td]
[td]johndoe@example.com[/td]
[td]345693887[/td]
[/tr]
[tr]
[td]Jane Doe[/td]
[td]janedoe@example.com[/td]
[td]685528874[/td]
[/tr]
[tr]
[td]Patrick Smith[/td]
[td]patricksmith@example.com[/td]
[td]5786255143[/td]
[/tr]
[/table]
[table bordered=success]
[tr]
[th]Name[/th]
[th]Email[/th]
[th]Phone number[/th]
[/tr]
[tr]
[td]John Doe[/td]
[td]johndoe@example.com[/td]
[td]345693887[/td]
[/tr]
[tr]
[td]Jane Doe[/td]
[td]janedoe@example.com[/td]
[td]685528874[/td]
[/tr]
[tr]
[td]Patrick Smith[/td]
[td]patricksmith@example.com[/td]
[td]5786255143[/td]
[/tr]
[/table]
[table=info bordered=danger]
[tr]
[th]Name[/th]
[th]Email[/th]
[th]Phone number[/th]
[/tr]
[tr]
[td]John Doe[/td]
[td]johndoe@example.com[/td]
[td]345693887[/td]
[/tr]
[tr]
[td]Jane Doe[/td]
[td]janedoe@example.com[/td]
[td]685528874[/td]
[/tr]
[tr]
[td]Patrick Smith[/td]
[td]patricksmith@example.com[/td]
[td]5786255143[/td]
[/tr]
[/table]
[/codeblock]
[table bordered=1]
[tr]
[th]Name[/th]
[th]Email[/th]
[th]Phone number[/th]
[/tr]
[tr]
[td]John Doe[/td]
[td]johndoe@example.com[/td]
[td]345693887[/td]
[/tr]
[tr]
[td]Jane Doe[/td]
[td]janedoe@example.com[/td]
[td]685528874[/td]
[/tr]
[tr]
[td]Patrick Smith[/td]
[td]patricksmith@example.com[/td]
[td]5786255143[/td]
[/tr]
[/table]
[table bordered=success]
[tr]
[th]Name[/th]
[th]Email[/th]
[th]Phone number[/th]
[/tr]
[tr]
[td]John Doe[/td]
[td]johndoe@example.com[/td]
[td]345693887[/td]
[/tr]
[tr]
[td]Jane Doe[/td]
[td]janedoe@example.com[/td]
[td]685528874[/td]
[/tr]
[tr]
[td]Patrick Smith[/td]
[td]patricksmith@example.com[/td]
[td]5786255143[/td]
[/tr]
[/table]
[table=info bordered=danger]
[tr]
[th]Name[/th]
[th]Email[/th]
[th]Phone number[/th]
[/tr]
[tr]
[td]John Doe[/td]
[td]johndoe@example.com[/td]
[td]345693887[/td]
[/tr]
[tr]
[td]Jane Doe[/td]
[td]janedoe@example.com[/td]
[td]685528874[/td]
[/tr]
[tr]
[td]Patrick Smith[/td]
[td]patricksmith@example.com[/td]
[td]5786255143[/td]
[/tr]
[/table]
[p]
With [i]Bootstrap[/i] you can also style individual rows or even cells with [code]primary, secondary, info, warning, danger, success, light[/code] or [code]dark[/code] applying to the tag.
[/p]
[codeblock]
[table]
[tr]
[th]Styling[/th]
[th]Cell[/th]
[th]Cell[/th]
[/tr]
[tr=primary]
[td]primary[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[tr=secondary]
[td]secondary[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[tr=info]
[td]info[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[tr=success]
[td]success[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[tr=warning]
[td]warning[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[tr=danger]
[td]danger[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[tr=light]
[td]light[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[tr=dark]
[td]dark[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[/table]
[/codeblock]
[table]
[tr]
[th]Styling[/th]
[th]Cell[/th]
[th]Cell[/th]
[/tr]
[tr=primary]
[td]primary[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[tr=secondary]
[td]secondary[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[tr=info]
[td]info[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[tr=success]
[td]success[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[tr=warning]
[td]warning[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[tr=danger]
[td]danger[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[tr=light]
[td]light[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[tr=dark]
[td]dark[/td]
[td]cell[/td]
[td]cell[/td]
[/tr]
[/table]
[h3]Rowspan and Colspan[/h3]
[p]
You can also create tables that span over mutliple rows or columns. Add the [code]rowspan=n[/code] or [code]colspan=n[/code] to the table header or table data ([i]n[/i] is the number of rows or columns that should be spanned).
[/p]
[codeblock]
[table bordered=1]
[tr]
[th]Header 1[/th]
[th]Header 2[/th]
[th]Header 3[/th]
[/tr]
[tr]
[td]Row1-Column1[/td]
[td]Row1-Column2[/td]
[td rowspan=2]Row1-Column3[/td]
[/tr]
[tr]
[td]Row2-Column1[/td]
[td]Row2-Column2[/td]
[/tr]
[tr]
[td]Row3-Column1[/td]
[td colspan=2]Row3-Column2[/td]
[/tr]
[/table]
[/codeblock]
[table bordered=1]
[tr]
[th]Header 1[/th]
[th]Header 2[/th]
[th]Header 3[/th]
[/tr]
[tr]
[td]Row1-Column1[/td]
[td]Row1-Column2[/td]
[td rowspan=2]Row1-Column3[/td]
[/tr]
[tr]
[td]Row2-Column1[/td]
[td]Row2-Column2[/td]
[/tr]
[tr]
[td]Row3-Column1[/td]
[td colspan=2]Row3-Column2[/td]
[/tr]
[/table]
[h2]Images[/h2]

View File

@@ -10,5 +10,11 @@
"content_type": "bbcode",
"status":"published",
"file":"bs-license.bbcode"
},
"tw-bbcode": {
"title": "BBCode used by TinyWiki",
"content_type": "bbcode",
"status":"published",
"file":"bbcode.bbcode"
}
}

View File

@@ -8,6 +8,10 @@ from django.contrib.auth import get_user_model
from tinywiki.enums import WIKI_CONTENT_TYPES, WIKI_PAGE_STATUS, WikiContentType, WikiPageStatus
import markdown
import bbcode as _bbcode
from tinywiki.parser import bbcode
from .parser import parse_bbcode
from . import settings
import tinywiki
@@ -122,3 +126,11 @@ class Image(models.Model):
uploaded_at = models.DateTimeField(_("uploaded at"),
auto_now_add=True)
@property
def description_html(self)->str:
return _bbcode.render_html(self.description)
@property
def description_html_safe(self)->SafeText:
return mark_safe(self.description_html)

View File

@@ -1,6 +1,6 @@
import bbcode
from . import formatters
PARSER = bbcode.Parser(newline="\n",escape_html=True)
PARSER = bbcode.Parser(newline="\n",escape_html=True,replace_links=False)
def _():
for i in formatters.SIMPLE_FORMATTERS:

View File

@@ -10,26 +10,36 @@ from .text_formatters import (
render_wiki_image,
render_wiki_link,
render_wiki_url,
)
from .simple_formatters import (
SIMPLE_HEADER_FORMATTERS,
render_table,
render_table_header,
render_table_data,
render_table_row,
render_youtube_video,
)
from .simple_formatters import SIMPLE_FORMATTERS
# a list of tuples containig an tuple args and a dict of kwargs
SIMPLE_FORMATTERS=[
*SIMPLE_HEADER_FORMATTERS,
]
#a list of tuples containing an tuple of args and a dict of kwargs
FORMATTERS=[
(('url',render_url),{'strip':True,'swallow_trailing_newline':True,'same_tag_closes':True}),
(('wiki-url',render_wiki_url),{'strip':True,'swallow_trailing_newline':True,'same_tag_closes':True}),
(('wiki',render_wiki_link),{'strip':True,'swallow_tailin_newline':True,'standalone':True}),
(('codeblock',render_codeblock),{'strip':False,'swallow_trailing_newline':False,'same_tag_closes':True}),
(('codeblock',render_codeblock),{'strip':True,'swallow_trailing_newline':False,'same_tag_closes':False,"render_embedded":False}),
(('ol',render_ordered_list),{}),
(('ul',render_unordered_list),{}),
(('li',render_list_item),{}),
(('p',render_paragraph),{}),
(('image',render_image),{}),
(('wiki-image',render_wiki_image),{'standalone':True})
(('p',render_paragraph),{'same_tag_closes':False}),
(('image',render_image),{'same_tag_closes':True}),
(('img',render_image),{'same_tag_closes':True}),
(('wiki-image',render_wiki_image),{'standalone':True}),
(('wimg',render_wiki_image),{'standalone':True}),
(('table',render_table),{}),
(('table-row',render_table_row),{}),
(('tr',render_table_row),{}),
(('table-header',render_table_header),{}),
(('th',render_table_header),{}),
(('table-data',render_table_data),{}),
(('td',render_table_data),{}),
(('youtube',render_youtube_video),{'same_tag_closes':True}),
]

View File

@@ -1,11 +1,15 @@
SIMPLE_HEADER_FORMATTERS = [
SIMPLE_FORMATTERS = [
(('h1',"<h1>%(value)s</h1>"),{}),
(('h2',"<h2>%(value)s</h2>"),{}),
(('h3',"<h3>%(value)s</h3>"),{}),
(('h4',"<h4>%(value)s</h4>"),{}),
(('h5',"<h5>%(value)s</h5>"),{}),
(('h6',"<h6>%(value)s</h6>"),{}),
(('mark',"<mark>%(value)s</mark>"),{}),
(("strong","<strong>%(value)s</strong>"),{}),
(("em","<em>%(value)s</em>"),{}),
(("br","<br>"),{"standalone":True}),
(('copy',"&copy;"),{'standalone':True}),
(('reg',"&reg;"),{'standalone':True}),
(('trade',"&trade;"),{'standalone':True}),

View File

@@ -2,9 +2,10 @@ from django_project.settings import STATIC_URL
from django.urls import reverse
from django.template.loader import render_to_string
from django.utils.translation import gettext as _
from ... import settings
from ... import models
import bbcode
def render_url(tag_name:str,value,options,parent,context):
try:
@@ -12,17 +13,19 @@ def render_url(tag_name:str,value,options,parent,context):
except KeyError:
url = value
if '://' not in url:
if not url.startswith('/') and '://' not in url:
url = "http://" + url
if settings.USE_BOOTSTRAP:
return f"<a href=\"{url}\" class=\"icon-link icon-link-hover\" referrer-policy=\"no-referrer\" rel=\"noreferrer noopener\">{value}{render_to_string('tinywiki/icons/box-arrow-up-right.svg')}</a>"
if ['noicon in options']:
return f"<a href=\"{url}\" referrer-policy=\"no-referrer\" rel=\"noreferrer noopener\">{value}</a>"
return f"<a href=\"{url}\" class=\"icon-link icon-link-hover\" referrer-policy=\"no-referrer\" rel=\"noreferrer noopener\">{value}<svg class=\"bi\"><use xlink:href=\"{settings.settings.STATIC_URL + "tinywiki/icons/bootstrap-icons.svg"}#box-arrow-up-right\"></use></svg></a>"
return f"<a href=\"{url}\" referrer-policy=\"no-referrer\" rel=\"noreferrer noopener\">{value}</a>"
def render_wiki_url(tag_name,value,options,parent,context):
if tag_name in options:
url = reverse("tinywiki:page",kwargs={'slug':options[tag_name]})
slug=options['tag_name']
slug=options[tag_name]
try:
page = models.Page.objects.get(slug=slug)
except models.Page.DoesNotExist:
@@ -33,47 +36,49 @@ def render_wiki_url(tag_name,value,options,parent,context):
slug=None
if settings.USE_BOOTSTRAP:
href = settings.settings.STATIC_URL+"tinywiki/icons/bootstrap-icons.svg"
if page:
if page.slug.startswith('tw-'):
svg=render_to_string('tinywiki/icons/journal.svg')
svg = "journal"
elif page.slug:
svg=render_to_string('tinywiki/icons/book.svg')
svg = "book"
else:
svg=render_to_string('tinywiki/icons/file-earmark-x')
return f"<a href=\"{url}\" class=\"icon-link icon-link-hover\">{value}{svg}</a>"
svg=href + "file-earmark-x"
return f"<a href=\"{url}\" class=\"icon-link icon-link-hover\">{value}<svg class=\"bi\"><use xlink:href=\"{href}#{svg}\"></use></svg></a>"
return f"<a href=\"{url}\">{value}</a>"
def render_wiki_link(tag_name,value,options,parent,context):
if tag_name in options:
slug = options['tag_name']
slug = options[tag_name]
print("slug",slug)
try:
page = models.Page.objects.get(slug=slug)
title = page.title
if slug.starts_with('tw-'):
svg = "tinywiki/icons/journal.svg"
if slug.startswith('tw-'):
svg = "journal"
else:
svg = "tinywiki/icons/book.svg"
except:
svg = "book"
except models.Page.DoesNotExist:
page = None
title = _("Page not found")
svg_template = "tinywiki/icons/file-earmark-x.svg"
svg = "file-earmark-x"
url = reverse("tinywiki:page",kwargs={'slug':slug})
else:
slug = None
title = _("Home")
url = reverse("tinywiki:home")
svg_template = "tinywiki/icons/house.svg"
svg = "house"
if settings.USE_BOOTSTRAP:
return f"<a href=\"{url}\" class=\"icon-link icon-link-hover\">{value}{render_to_string(svg_template)}</a>"
href = settings.settings.STATIC_URL + "tinywiki/icons/bootstrap-icons.svg"
return f"<a href=\"{url}\" class=\"icon-link icon-link-hover\">{title}<svg class=\"bi\"><use xlink:href=\"{href}#{svg}\"></use></svg></a>"
return f"<a href=\"{url}\">{value}</a>"
def render_codeblock(tag_name:str,value,options,parent,context)->str:
if 'codeblock' in options:
return f"<pre><code class=\"language-{options['codeblock']}\">{value}</pre></code>"
return f"<pre><code>{value}</pre></code>"
if tag_name in options:
return f"<pre style=\"overflow-x:auto;\"><code class=\"language-{options[tag_name]}\">{value}</pre></code>"
return f"<pre style=\"overflow-x:auto;\"><code>{value}</pre></code>"
def render_ordered_list(tag_name:str,value,options,parent,context)->str:
return f"<ol>{value}</ol>"
@@ -112,7 +117,7 @@ def render_image(tag_name:str,value,options,parent,context):
if 'width' in options:
_w = options['width']
if _w.endswith('px'):
if _w.endswith('px') or _w.endswith('em') or _w.endswith('rem'):
fig_styles.append(f"width:{_w};")
else:
if _w.endswith('%'):
@@ -129,16 +134,41 @@ def render_image(tag_name:str,value,options,parent,context):
fig_classes.append(f'w-{width}')
else:
fig_styles.append(f"width:{_w}%;")
if 'height' in options:
_h = options['width']
if _h.endswith('px') or _h.endswith('em') or _h.endswith('rem'):
fig_styles.append(f"height:{_h};")
else:
if _h.endswith('%'):
_h= _h[:-1]
if _h.isdigit():
_h=int(_w)
if _h > 100:
_h = 100
if settings.USE_BOOTSTRAP:
if 1 < int(_h) <= 25:
height = 25
else:
height = ((_h // 25) * 25)
if height > 100:
height = 100
fig_classes.append(f'h-{height}')
else:
fig_styles.append(f"height:{_h}%;")
if "position" in options:
pos = options['position']
if settings.USE_BOOTSTRAP:
if pos == "left" or pos=="start":
fig_classes += ["float-start","me-2"]
classes += ["float-start","me-2"]
elif pos == "right" or pos == "end":
fig_classes += ["float-end","ms-2"]
classes += ["float-end","ms-2"]
elif pos == "center":
fig_classes += ["mx-auto","d-block"]
classes += ["mx-auto","d-block"]
if styles:
style=f"style=\"{"".join(styles)}\""
@@ -150,7 +180,7 @@ def render_image(tag_name:str,value,options,parent,context):
else:
fig_style=""
if settings.USE_BOOTSTRAP:
return f'<figure class="{" ".join(fig_classes)} {fig_style}"><img src="{options[tag_name]}" class="{' '.join(classes)}" alt="{alt}" {style}><figcaption class="figure-caption text-end">{value}</figcaption></figure>'
return f'<figure class="{" ".join(fig_classes)} {fig_style}"><img src="{options[tag_name]}" class="{' '.join(classes)}" alt="{alt}" {style}><figcaption class="figure-caption text-end">{ value }</figcaption></figure>'
else:
return f'<figure {fig_style}><img src="{options[tag_name]}" {style}><figcaption>{value}</figcaption></figure>'
@@ -176,7 +206,7 @@ def render_wiki_image(tag_name:str,value,options,parent,context):
if 'width' in options:
_w = options['width']
if _w.endswith('px'):
if _w.endswith('px') or _w.endswith('em') or _w.endswith('rem'):
fig_styles.append(f"width:{_w};")
else:
if _w.endswith('%'):
@@ -193,6 +223,28 @@ def render_wiki_image(tag_name:str,value,options,parent,context):
fig_classes.append(f'w-{width}')
else:
fig_styles.append(f"width:{_w}%;")
if 'height' in options:
_h = options['width']
if _h.endswith('px') or _h.endswith('em') or _h.endswith('rem'):
fig_styles.append(f"height:{_h};")
else:
if _h.endswith('%'):
_h= _h[:-1]
if _h.isdigit():
_h=int(_w)
if _h > 100:
_h = 100
if settings.USE_BOOTSTRAP:
if 1 < int(_h) <= 25:
height = 25
else:
height = ((_h // 25) * 25)
if height > 100:
height = 100
fig_classes.append(f'h-{height}')
else:
fig_styles.append(f"height:{_h}%;")
if "position" in options:
pos = options['position']
@@ -215,7 +267,155 @@ def render_wiki_image(tag_name:str,value,options,parent,context):
fig_style=""
if settings.USE_BOOTSTRAP:
return f'<figure class="{" ".join(fig_classes)}" {fig_style}><img src="{image.image.url}" alt="{image.alt}" class="{' '.join(classes)}" {style}><figcaption class="figure-caption text-end">{image.description}</figcaption></figure>'
return f'<figure class="{" ".join(fig_classes)}" {fig_style}><img src="{image.image.url}" alt="{image.alt}" class="{' '.join(classes)}" {style}><figcaption class="figure-caption text-end">{image.description_html}</figcaption></figure>'
else:
return f'<figure {fig_style}><img src="{image.image.url}" alt="{image.alt}" {style}><figcaption>{image.description}</figcaption></figure>'
def render_table(tag_name:str,value,options,parent,context):
if settings.USE_BOOTSTRAP:
classes=["table"]
if "bordered" in options:
if options["bordered"] not in ("0","n","no","false","off"):
classes.append("table-bordered")
if options["bordered"] in ("primary","secondary","info","warning","danger","success","light","dark"):
classes.append(f"border-{options['bordered']}")
if tag_name in options:
if options[tag_name] in ("primary","secondary","info","warning","danger","success","light","dark"):
classes.append(f"table-{options[tag_name]}")
return f"<table class=\"{" ".join(classes)}\">{value}</table>"
return f"<table>{value}</table>"
def render_table_row(tag_name:str,value,options,parent,context):
classes=[]
if settings.USE_BOOTSTRAP:
if tag_name in options:
if options[tag_name] in ("primary","secondary","info","warning","danger","success","light","dark"):
classes.append(f"table-{options[tag_name]}")
class_attr=f"class=\"{" ".join(classes)}\"" if classes else ""
return f"<tr {class_attr}>{value}</tr>"
def render_table_header(tag_name:str,value,options,parent,context):
extra_attributes=[]
classes=[]
if "colspan" in options:
extra_attributes.append(f"colspan=\"{options['colspan']}\"")
if "rowspan" in options:
extra_attributes.append(f"rowspan=\"{options['rowspan']}\"")
if settings.USE_BOOTSTRAP:
if tag_name in options:
if options[tag_name] in ("primary","secondary","info","warning","danger","success","light","dark"):
classes.append(f"table-{options[tag_name]}")
class_attr=f"class=\"{" ".join(classes)}\"" if classes else ""
return f"<th {class_attr} {" ".join(extra_attributes)}>{value}</th>"
def render_table_data(tag_name:str,value,options,parent,context):
extra_attributes=[]
classes=[]
if "colspan" in options:
extra_attributes.append(f"colspan=\"{options['colspan']}\"")
if "rowspan" in options:
extra_attributes.append(f"rowspan=\"{options['rowspan']}\"")
if settings.USE_BOOTSTRAP:
if tag_name in options:
if options[tag_name] in ("primary","secondary","info","warning","danger","success","light","dark"):
classes.append(f"table-{options[tag_name]}")
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 ""
if 'alt' in options:
alt=options['alt']
else:
alt=""
if settings.USE_BOOTSTRAP:
styles=[]
classes=["w-100"]
div_classes=["my-1"]
div_styles=[]
else:
styles=["max-width:100%;"]
classes=[]
div_classes=[]
div_styles=[]
if 'width' in options:
_w = options['width']
if _w.endswith('px') or _w.endswith('em') or _w.endswith('rem'):
if settings.USE_BOOTSTRAP:
div_styles.append(f"width:{_w};")
styles.append(f"width:{_w};")
else:
if _w.endswith('%'):
_w = _w[:-1]
if _w.isdigit():
_w=int(_w)
if _w > 100:
_w = 100
if settings.USE_BOOTSTRAP:
if 1 < int(_w) <= 25:
width = 25
else:
width = ((_w // 25) * 25)
div_classes.append(f'w-{width}')
else:
styles.append(f"width:{_w}%;")
if 'height' in options:
_h = options['width']
if _h.endswith('px') or _h.endswith('em') or _h.endswith('rem'):
if settings.USE_BOOTSTRAP:
div_styles.append(f"height:{_h};")
else:
styles.append(f"height:{_h};")
else:
if _h.endswith('%'):
_h= _h[:-1]
if _h.isdigit():
_h=int(_w)
if _h > 100:
_h = 100
if settings.USE_BOOTSTRAP:
if 1 < int(_h) <= 25:
height = 25
else:
height = ((_h // 25) * 25)
if height > 100:
height = 100
div_classes.append(f'h-{height}')
else:
styles.append(f"height:{_h}%;")
if "position" in options:
pos = options['position']
if settings.USE_BOOTSTRAP:
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"]
elif pos == "center":
div_classes += ["mx-auto","d-block"]
#classes += ["mx-auto","d-block"]
if styles:
style=f"style=\"{"".join(styles)}\""
else:
style=""
if div_styles:
div_style=f'style="{"".join(div_styles)}"'
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>'
else:
return f'<div {div_style}><iframe src="https://www.youtube.com/embed/{options[tag_name]}?rel=0" allowfullscreen></iframe></div>'

View File

@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}

View File

@@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#c18401}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}

View File

@@ -0,0 +1,38 @@
/*
Language: bbcode
Author: Paul Reid <paul@reid-family.org>
Description: highlightjs language definition for bbcode files
Category: config
*/
hljs.registerLanguage('bbcode', function (hljs) {
return {
case_insensitive: true,
contains: [
{
className: 'name',
begin: /\[[^=\s\]]*/
},
{
className: 'name',
begin: ']'
},
{
className: 'attribute',
begin: /(?<==)[^\]\s]*/
},
{
className: 'attr',
begin: /(?<=\[[^\]]* )[^\s=\]]*/
},
{
className: 'string',
begin: /[=;:8]'?\-?[\)\(3SPDO>@$|/]/
},
{
className: 'string',
begin: /:[\w]*:/
}
]
};
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TinyWiki</title>
<title>{% block head_title %}TinyWiki{% endblock %}</title>
{% block extra_css %}{% endblock %}
{% block scripts %}{% endblock %}
</head>

View File

@@ -12,12 +12,15 @@
{% else %}
<h1>Welcome to TinyWiki</h1>
<p>{% blocktranslate %}You are seeing this welcome page because there is no Welcome page
configured for your Wiki. To configure a welcome page create a new page with the
slug <i>tw-home</i> and put the content for your welcome-page there.{% endblocktranslate %}</p>
<p>{% blocktranslate %}You can use Markdown or BBCode to write your pages. If you don't know
Markdown read the <a href="#">Guide for Markdown used by TinyWiki</a>. Or if you want to use
BBCode there is a <a href="#">Guide for BBCode used by TinyWiki</a> too.{% endblocktranslate %}
<p style="text-align:justify">{% blocktranslate %}You are seeing this welcome page because there is no Welcome page
configured for your Wiki. To configure a welcome page {{ create_tw_home }} and put the content for your welcome-page there.{% endblocktranslate %}</p>
<p style="text-align:justify">{% blocktranslate %}You can use BBCode or Markdown to write your pages. If you don't know
Markdown or BBCode there are two guides you can consult before you start editing your pages.{% endblocktranslate %}
<ol>
<li>{{ bbcode_guide }}</li>
<li>{{ markdown_guide }}</li>
</ol>
</p>
{% endif %}
{% endblock content %}

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-book" viewBox="0 0 16 16">
<path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783"/>
</svg>

Before

Width:  |  Height:  |  Size: 772 B

View File

@@ -1,4 +0,0 @@
<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-box-arrow-up-right\" viewBox=\"0 0 16 16\">
<path fill-rule=\"evenodd\" d=\"M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5\"/>
<path fill-rule=\"evenodd\" d=\"M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z\"/>
</svg>

Before

Width:  |  Height:  |  Size: 552 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-x" viewBox="0 0 16 16">
<path d="M6.854 7.146a.5.5 0 1 0-.708.708L7.293 9l-1.147 1.146a.5.5 0 0 0 .708.708L8 9.707l1.146 1.147a.5.5 0 0 0 .708-.708L8.707 9l1.147-1.146a.5.5 0 0 0-.708-.708L8 8.293z"/>
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 485 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-house" viewBox="0 0 16 16">
<path d="M8.707 1.5a1 1 0 0 0-1.414 0L.646 8.146a.5.5 0 0 0 .708.708L2 8.207V13.5A1.5 1.5 0 0 0 3.5 15h9a1.5 1.5 0 0 0 1.5-1.5V8.207l.646.647a.5.5 0 0 0 .708-.708L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293zM13 7.207V13.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V7.207l5-5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 415 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-journal" viewBox="0 0 16 16">
<path d="M3 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-1h1v1a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v1H1V2a2 2 0 0 1 2-2"/>
<path d="M1 5v -.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/>
</svg>

Before

Width:  |  Height:  |  Size: 498 B

View File

@@ -0,0 +1,35 @@
{% extends "tinywiki/page/bs-model-base.html" %}
{% load i18n %}
{% block head_title %}{{ site_title }} - {% translate "Create a new Wiki page" %}{% endblock %}
{% block title %}{% translate "Create a new Wiki page" %}{% endblock title%}
{% block scripts %}
<script>
function save_and_continue() {
form = document.getElementById("wiki-page-form");
form.action = form.action + "?save=1";
form.submit();
}
</script>
{% endblock scripts %}
{% block form_buttons %}
<button class="btn btn-primary" type="button" onclick="save_and_continue();">{% translate "Create page and continue" %}<button>
<button class="btn btn-success">{% translate "Create Page" %}</button>
<a class="btn btn-secondary" href="{% url 'tinywiki:home' %}">{% translate "Cancel" %}</a>
{% endblock form_buttons %}
{% block extended_extra_scripts %}
<script>
document.addEventListener('keydown',function(e) {
if (e.ctrlKey && e.key == "s") {
e.preventDefault();
var form = document.getElementById('wiki-page-form');
form.action = form.action + "?save=1";
form.submit();
}
});
</script>
{% endblock extended_extra_scripts%}

View File

@@ -0,0 +1,45 @@
{% extends "tinywiki/page/bs-model-base.html" %}
{% load i18n static %}
{% block head_title %}{{ site_title }} - {% translate "Edit Wiki Page" %}{% endblock head_title %}
{% block title %}{% translate "Edit Wiki Page" %}{% endblock title%}
{% block scripts %}
<script src="{% static "tinywiki/js/htmx.min.js" %}"></script>
{% endblock %}
{% block form_buttons %}
<span id="popover-btn-save-and-continue">
<button id="btn-save-and-continue"
type="button"
class="btn btn-primary"
id="btn-save-and-continue"
data-bs-toggle="popopver"
data-bs-placement="top"
hx-target="#popover-btn-save-and-continue"
hx-post="{% url "tinywiki:hx-page-edit" pk=page.pk %}"
>
{% translate "Save and continue" %}
</button>
</span>
<button type="submit" class="btn btn-success">{% translate "Save and show" %}</button>
<a class="btn btn-secondary" href="{% url "tinywiki:page" form.instance.slug %}">{% translate "Cancel" %}</a>
{% endblock form_buttons %}
{% block extended_extra_scripts%}
<script>
document.addEventListener('keydown',function(e) {
if (e.ctrlKey && e.key == "s") {
e.preventDefault();
btn = document.getElementById("btn-save-and-continue")
btn.click()
}
});
</script>
<script>
btn_save_popover = new bootstrap.Popover(document.getElementById("btn-save-and-continue"),trigger="manual");
btn_save_popover.show();
setTimeout(()=>{btn_save_popover.hide();},3000);
</script>
{% endblock %}

View File

@@ -0,0 +1,66 @@
{% extends base_template %}
{% load i18n widget_tweaks %}
{% block style %}
{% endblock %}
{% block content %}
<h1>{% block title %}{% endblock title%}</h1>
<form method="POST"
class="mb-4"
id="wiki-page-form"
action="{% if create %}{% url "tinywiki:page-create" %}{% else %}{% url "tinywiki:page-edit" slug=form.instance.slug|default:"1" %}{% endif %}">
{% csrf_token %}
<div class="form-floating form-floating-lg mb-3">
{% render_field form.title class="form-control form-control-lg" %}
{{ form.title.label_tag }}
</div>
<div class="form-floating form-floating-sm mb-3">
{% if create and slug %}
{% render_field form.slug class="form-control form-control-sm" value=slug placeholder="slug" %}
{% else %}
{% render_field form.slug class="form-control form-control-sm" palceholder="slug" %}
{% endif %}
{{ form.slug.label_tag }}
</div>
<div class="row">
<div class="col-lg-6">
<div class="form-floating form-floating-sm mb-3">
{% render_field form.content_type_data class="form-select form-select-sm" %}
{{ form.content_type_data.label_tag }}
</div>
</div>
<div class="col-lg-6">
<div class="form-floating form-floating-sm mb-3">
{% render_field form.status_data class="form-select form-select-sm" %}
{{ form.status_data.label_tag }}
</div>
</div>
</div>
<div class="form-floating mb-3">
{% render_field form.content class="form-control h-100" style="font-family:monospace;" rows=25 %}
{{ form.content.label_tag }}
</div>
<div class="text-end">
{% block form_buttons %}{% endblock form_buttons %}
</div>
</form>
{% endblock content%}
{% block extra_scripts %}
<script>
document.getElementById("id_content").addEventListener('keydown',function(e) {
if (e.key == "Tab") {
e.preventDefault();
var start = this.selectionStart;
var end = this.selectionEnd;
console.log(this.value);
// set textarea value to: text before caret + 4 spaces + text after caret
this.value = this.value.substring(0,start) + " " + this.value.substring(end);
this.selectionStart = this.selectionEnd = start + 4;
}
});
</script>
{% block extended_extra_scripts %}{% endblock %}
{% endblock extra_scripts %}

View File

@@ -1,11 +1,19 @@
{% extends base_template %}
{% load i18n %}
{% load i18n static %}
{% block extra_css %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/default.min.css">
<link rel="stylesheet" id="hljs-theme" href="{% static 'tinywiki/css/atom-one-light.min.css' %}">
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js"></script>
<script>
const theme=(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
if (theme==="dark") {
let hljs_theme = document.getElementById('hljs-theme');
hljs_theme.setAttribute('href',href="{% static 'tinywiki/css/atom-one-dark.min.css' %}");
}
</script>
<script src="{% static 'tinywiki/js/highlight.min.js' %}"></script>
<script src="{% static 'tinywiki/js/bbcode.js' %}"></script>
{% endblock %}
{% block extra_scripts %}
@@ -18,9 +26,24 @@
<a class="btn btn-primary" href="{% url 'tinywiki:page-edit' slug=page.slug %}">{% translate "Edit Page" %}</a>
{% endif %}
{% if user_can_delete_wiki_page %}
<button class="btn btn-danger" type="button">Delete Page</button>
<button class="btn btn-danger"
hx-get="{% url 'tinywiki:hx-page-delete' pk=page.pk %}"
hx-target="#modal-here"
hx-swap="innerHTML"
hx-trigger="clicked"
data-bs-toggle="modal"
data-bs-target="#modal-here"
>{% translate "Delete Page" %}</button>
{% endif %}
</div>
<h1>{{ page.title }}</h1>
{{ page.html_content }}
{% endblock content %}
{% block extra_body %}
<div id="modal-here" class="modal modal-blur fade" style="display:none;" aria-hidden="false" tab-index="-1">
<div class="modal-dialog modal-md modal-dialog-centered" role="document">
<div class="modal-content"></div>
</div>
</div>
{% endblock extra_body%}

View File

@@ -4,13 +4,6 @@
{% block title %}{% translate "Create a new Wiki page" %}{% endblock title%}
{% block form_buttons %}
{% if use_bootstrap %}
<div class="text-end">
<button class="btn btn-primary">{% translate "Create Page" %}</button>
<a class="btn btn-secondary" href="{% url 'tinywiki:home' %}">{% translate "Cancel" %}</a>
</div>
{% else %}
<button type="submit">{% translate "Create Page" %}</button>
<a class="button button-secondary" href="{% url 'tinywiki:home' %}">{% translate "Cancel" %}</a>
{% endif %}
{% endblock form_buttons %}

View File

@@ -0,0 +1,12 @@
{% extends base_template %}
{% load i18n %}
{% block content %}
<form method="POST">
{% csrf_token %}
<p>Type <b>{{ page.slug }}</b> in the field to delete the page.</p>
{{ form.as_p }}
<button class="button button-danger" type="submit">{% translate "Delete Page" %}</button>
<a class="button button-secondary" href="{% url 'tinywiki:page' page.slug %}">Cancel</a>
</form>
{% endblock content %}

View File

@@ -4,13 +4,6 @@
{% block title %}{% translate "Edit Wiki Page" %}{% endblock title%}
{% block form_buttons %}
{% if use_bootstrap %}
<div class="text-end">
<button type="submit" class="btn btn-primary">{% translate "Save Changes" %}</button>
<a class="btn btn-secondary" href="{% url "tinywiki:page" form.instance.slug %}">{% translate "Cancel" %}</a>
</div>
{% else %}
<button type="submit">{% translate "Save Changes" %}</button>
<a class="button button-secondary" href="{% url "tinywiki:page" form.instance.slug %}">{% translate "Cancel" %}</a>
{% endif %}
{% endblock form_buttons %}

View File

@@ -0,0 +1,27 @@
{% load i18n widget_tweaks %}
<div class="modal-dialog modal-md modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">{% translate "Delete Wiki-Page" %}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="POST">
{% csrf_token %}
<p>
{% with page.slug as page_slug %}
{% blocktranslate %}Type <b>{{ page_slug }}</b> in the field below to delete the Wiki-Page.{% endblocktranslate %}
{% endwith %}
<div class="form-floating form-floating-sm">
{% render_field form.slug class="form-control form-cotrol-sm" placeholder=page.slug %}
<label for="{{ form.slug.auto_id }}">{{ form.slug }}</label>
</div>
<div class="text-end">
<button class="btn btn-danger" type="submit">{% translate "Delete the page" %}</button>
<button clasS="btn btn-secondary" type="button" data-bs-dismiss="modal">{% translate "Cancel" %}</button>
</div>
</p>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,26 @@
{% load i18n %}
<span id="popover-btn-save-and-continue">
<button type="button"
class="btn btn-primary"
id="btn-save-and-continue"
data-bs-toggle="popopver"
data-bs-placement="top"
hx-target="#popover-btn-save-and-continue"
hx-post="{% url "tinywiki:hx-page-edit" pk=page.pk %}"
{% if save_success %}
data-bs-title="Successfully saved"
data-bs-content="Your Wiki page has been successfully saved!"
{% else %}
data-bs-title="Saving failed"
data-bs-content="Your Wiki page could not be saved!"
{% endif %}
>
Save and continue
</button>
<script>
btn_save_popover = new bootstrap.Popover(document.getElementById("btn-save-and-continue"),trigger="manual");
btn_save_popover.show();
setTimeout(() => {btn_save_popover.hide();},3000)
</script>
</span>

View File

@@ -4,77 +4,44 @@
{% block content %}
<h1>{% block title %}{% endblock title%}</h1>
<form method="POST"
class="mb-4"
id="wiki-page-form"
action="{% if create %}{% url "tinywiki:page-create" %}{% else %}{% url "tinywiki:page-edit" slug=form.instance.slug|default:"1" %}{% endif %}">
{% csrf_token %}
{% if use_bootstrap %}
<div class="form-floating mb-2">
{% with "form-control form-control-lg "|add:title_extra as title_class %}
{% render_field form.title class=title_class placeholder="{{ form.title.label }}" %}
{% endwith %}
<label for="{{ form.title.auto_id }}">{{ form.title.label }}</label>
</div>
<div class="form-floating mb-2">
{% with "form-control form-control-sm "|add:slug_extra as slug_class%}
{% render_field form.slug class=slug_class placeholder="{{ form.slug.label }}" %}
{% endwith %}
<label for="{{ form.slug.auto_id }}">{{ form.slug.label }}</label>
</div>
<div class="row">
<div class="col-md-6 mb-2">
<div class="form-floating mb-2">
{% with "form-select form-select-sm "|add:content_type_extra as content_type_class %}
{% render_field form.content_type_data class=content_type_class %}
{% endwith %}
<label for="{{ for.content_type_data.auto_id }}">{{ form.content_type_data.label }}</label>
</div>
</div>
<div class="col-md-6">
<div class="form-floating mb-2">
{% with "form-select form-select-sm "|add:status_extra as status_class %}
{% render_field form.status_data class=status_class %}
{% endwith %}
<label for="{{ for.status_data.auto_id }}">{{ form.status_data.label }}</label>
</div>
</div>
</div>
<div class="form-floating mb-3">
{% with "form-control h-100 "|add:content_extra as content_class %}
{% render_field form.content class=content_class rows="25"%}
{% endwith %}
<label for="{{ form.content.auto_id }}">{{ form.content.label }}</label>
</div>
<div class="text-end">
</div>
{% else %}
<p>
{% with title_extra|default:"" as title_class %}
{{ form.title.label_tag }}{% render_field form.title class=Title_class %}
{{ form.title.label_tag }} {% render_field form.title class="{{ title_class }}" %}
{% endwith %}
</p>
<p>
{% with slug_extra|default:"" as slug_class %}
{{ form.slug.label_tag }}{% render_field form.slug class=slug_class %}
{{ form.slug.label_tag }}
{% with slug_extra|default:"" as slug_class%}
{% if create and slug %}
{% render_field form.slug class=slug_class value=slug placeholder="slug" %}
{% else %}
{% render_field form.slug class=slug_class palceholder="slug" %}
{% endif %}
{% endwith %}
</p>
<p>
{% with content_type_extra|default:"" as content_type_class %}
{{ form.content_type_data.label_tag }}{% render_field form.content_type_data class=content_type_class %}
{{ form.content_type_data.label_tag }} {% render_field form.content_type_data class=content_type_class %}
{% endwith %}
</p>
<p>
{% with status_extra|default:"" as status_class %}
{{ form.status_data.label_tag }}{% render_field form.status_data class=status_class %}
{{ form.status_data.label_tag }} {% render_field form.status_data class=status_class %}
{% endwith %}
</p>
<p>
{% with content_extra|default:"" as content_class %}
{{ form.content.label_tag }}{% render_field form.content class=content_class cols=80 rows=30 %}
{{ form.content.label_tag }} {% render_field form.content class=content_class cols=80 rows=30 %}
{% endwith %}
</p>
{% endif %}
<div class="text-end">
{% block form_buttons %}{% endblock form_buttons %}
</div>
</form>
{% endblock content%}

View File

@@ -1,11 +1,13 @@
{% extends base_template %}
{% load i18n %}
{% load i18n static %}
{% block extra_css %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/default.min.css">
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js"></script>
<script src="{% static 'tinywiki/js/htmx.min.js' %}"></script>
<script src="{% static 'tinywiki/js/highlight.min.js' %}"></script>
<script src="{% static 'tinywiki/js/bbcode.js' %}"></script>
{% endblock %}
{% block extra_scripts %}
@@ -17,8 +19,11 @@
<a href="{% url 'tinywiki:page-edit' slug=page.slug %}">{% translate "Edit Page" %}</a>
{% endif %}
{% if user_can_delete_wiki_page %}
<a href="#">Delete Page</a>
<a href="{% url 'tinywiki:page-delete' slug=page.slug %}">{% translate "Delete Page" %}</a>
{% endif %}
<h1>{{ page.title }}</h1>
{{ page.html_content }}
{% endblock content %}
{% block extra_body %}
{% block %}

View File

@@ -10,4 +10,7 @@ urlpatterns = [
path("page/<slug:slug>/",PageView.as_view(),name="page"),
path("page-create/",PageCreateView.as_view(),name="page-create"),
path("page/<slug:slug>/edit/",PageEditView.as_view(),name='page-edit'),
path("page/<slug:slug>/dekete/",PageDeleteView.as_view(),name='page-delete'),
path("__hx__/page/<int:pk>/edit/",HxPageEditView.as_view(),name='hx-page-edit'),
path("__hx__/page/<int:pk>/delete/",HxPageDeleteView.as_view(),name='hx-page-delete'),
]

View File

@@ -6,6 +6,57 @@ from typing import Any
class Base:
base_template_name = settings.TINYWIKI_BASE_TEMPLATE
@property
def user_can_edit_pages(self)->bool:
if (self.request.user.is_staff
or self.request.user.has_perm('page.tiynwiki-edit')
or self.request.user.has_perm('page.tinywiki-edit-all')):
return True
return False
@property
def user_can_edit_all_pages(self)->bool:
if (self.request.user.is_staff
or self.request.user.has_perm('page.tinywiki-edit-all')):
return True
return False
@property
def user_can_edit_system_pages(self)->bool:
if (self.request.user.is_staff
or self.request.user.has_perm('page.tiynwiki-edit-system')):
return True
return False
@property
def user_can_create_pages(self)->bool:
if (self.request.user.is_staff
or self.request.user.has_perm('page.tiynwiki-create')):
return True
return False
@property
def user_can_create_system_pages(self)->bool:
if (self.request.user.is_staff
or self.request.user.has_perm('page.tiynwiki-create-system')):
return True
return False
def page_is_creatable(self,slug:str)->bool:
if not slug:
return False
if slug.startswith('tw-'):
return self.user_can_create_system_pages
return self.user_can_create_pages
def page_is_editable(self,page)->bool:
if page.slug.startswith('tw-'):
return self.user_can_edit_system_pages
elif (self.user_can_edit_all_pages
or (self.request.user.pk == page.author.pk and self.user_can_edit_pages)):
return True
return False
@classmethod
def get_base_template_name(cls)->str:
return cls.base_template_name

View File

@@ -1,6 +1,9 @@
from curses.ascii import isalpha
from django.shortcuts import render
from django.utils.safestring import mark_safe
from django.urls import reverse
from django_project.settings import STATIC_URL
from tinywiki import settings
from .base import View
from django.http import HttpRequest,HttpResponse
@@ -18,12 +21,34 @@ class HomeView(View):
def get(self,request):
try:
page = Page.objects.get(slug='tw-home')
if (not Page.status == WikiPageStatus.PUBLISHED
and not request.user.is_staff
and not request.user.has_perm('page.view-all')):
page = None
except Page.DoesNotExist:
page = None
if self.user_can_create_system_pages:
if settings.USE_BOOTSTRAP:
create_tw_home = f"<a class=\"icon-link icon-link-hover\" href={reverse("tinywiki:page",kwargs={'slug':'tw-home'})}>{_('create a new page with the slug <i>tw-home</i>')}<svg class=\"bi\"><use xlink:href=\"{settings.settings.STATIC_URL + 'tinywiki/icons/bootstrap-icons.svg' }#house-add\" ></use></svg></a>"
else:
create_tw_home = f"<a href={reverse("tinywiki:page",kwargs={'slug':'tw-home'})}>{_('create a new page with the slug <i>tw-home</i>')}</a>"
else:
create_tw_home = "create a new page with the slug <i>tw-home</i>"
if settings.USE_BOOTSTRAP:
markdown_guide = f"<a class=\"icon-link icon-link-hover\" href={reverse('tinywiki:page',kwargs={'slug':'tw-markdown'})}>{_('Guide for markdown used by TinyWiki')}<svg class=\"bi\"><use xlink:href=\"{settings.settings.STATIC_URL + 'tinywiki/icons/bootstrap-icons.svg' }#journal\"></use></svg></a>"
bbcode_guide = f"<a class=\"icon-link icon-link-hover\" href={reverse('tinywiki:page',kwargs={'slug':'tw-bbcode'})}>{_('Guide for BBCode used by TinyWiki')}<svg class=\"bi\"><use xlink:href=\"{settings.settings.STATIC_URL + 'tinywiki/icons/bootstrap-icons.svg' }#journal\"></use></svg></a>"
else:
markdown_guide = f"<a class=\"icon-link icon-link-hover\" href={reverse('tinywiki:page',kwargs={'slug':'tw-markdown'})}>{_('Guide for markdown used by TinyWiki')}</a>"
bbcode_guide = f"<a class=\"icon-link icon-link-hover\" href={reverse('tinywiki:page',kwargs={'slug':'tw-bbcode'})}>{_('Guide for BBCode used by TinyWiki')}</a>"
return render(request,
self.get_template_name(),
self.get_context_data(page=page))
self.get_context_data(page=page,
user_can_create_system_pages=self.user_can_create_system_pages,
create_tw_home=mark_safe(create_tw_home),
markdown_guide=mark_safe(markdown_guide),
bbcode_guide=mark_safe(bbcode_guide)))
class TocView(View):
template_name = "tinywiki/home/wiki-content.html"

19
tinywiki/views/image.py Normal file
View File

@@ -0,0 +1,19 @@
from .base import FormView
class ImageUplodadView(FormView):
pass
class ImageEditView(FormView):
pass
class ImageDeleteFiew(FormView):
pass
class HxImageUplaodView(FormView):
pass
class HxImageEditView(FormView):
pass
class HxImageDeleteView(FormView):
pass

View File

@@ -1,15 +1,16 @@
from django.shortcuts import render,get_object_or_404,redirect
from django.urls import reverse, reverse_lazy
from django.http import HttpRequest,HttpResponse
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
from ..models import Page
from .base import View,FormView
from ..forms import PageForm,PageAdminForm
from ..forms import PageForm,PageAdminForm,PageDeleteForm
class PageView(View):
template_name = "tinywiki/page/view.html"
@@ -49,13 +50,19 @@ class PageView(View):
def get(self,request:HttpRequest,slug:str)->HttpResponse:
page = get_object_or_404(Page,slug=slug)
try:
page = Page.objects.get(slug=slug)
except Page.DoesNotExist:
if self.page_is_creatable(slug):
return redirect(reverse('tinywiki:page-create') + f"?slug={slug}")
raise Http404()
return render(request,
self.get_template_name(),
self.get_context_data(page=page))
class PageCreateView(LoginRequiredMixin,UserPassesTestMixin,FormView):
template_name = "tinywiki/page/create.html"
bs_template_name = "tinywiki/page/bs-create.html"
form_class = PageForm
def test_func(self) -> bool:
@@ -65,14 +72,26 @@ class PageCreateView(LoginRequiredMixin,UserPassesTestMixin,FormView):
return self.request.user.has_perm('tinywiki.tinywiki-create')
return False
def get_template_name(self) -> str:
if settings.USE_BOOTSTRAP:
return self.bs_template_name
return self.template_name
def get(self,request):
return render(request,
self.get_template_name(),
self.get_context_data())
def get_context_data(self,**kwargs):
context = super().get_context_data(**kwargs)
context['create']=True
context['slug'] = self.request.GET.get('slug',None)
context.setdefault("title_extra","")
context.setdefault("slug_extra","")
context.setdefault("content_type_extra","")
context.setdefault("status_extra","")
context.setdefault("content_extra","")
return context
def form_invalid(self, form):
@@ -149,6 +168,7 @@ class PageCreateView(LoginRequiredMixin,UserPassesTestMixin,FormView):
class PageEditView(LoginRequiredMixin,UserPassesTestMixin,FormView):
template_name = "tinywiki/page/edit.html"
bs_template_name = "tinywiki/page/bs-edit.html"
form_class = PageForm
def test_func(self) -> bool:
@@ -156,9 +176,16 @@ class PageEditView(LoginRequiredMixin,UserPassesTestMixin,FormView):
if self.request.user.is_staff:
return True
return False
def get_context_data(self,**kwargs):
def get_template_name(self) -> str:
if settings.USE_BOOTSTRAP:
return self.bs_template_name
return self.template_name
def get_context_data(self,page,**kwargs):
context = super().get_context_data(**kwargs)
context['create']=False
context['page']=page
context.setdefault("title_extra","")
context.setdefault("slug_extra","")
context.setdefault("content_type_extra","")
@@ -195,6 +222,7 @@ class PageEditView(LoginRequiredMixin,UserPassesTestMixin,FormView):
return render(self.request,
self.get_template_name(),
self.get_context_data(
page=self.instance,
slug_extra=slug_extra,
title_extra=title_extra,
status_extra=status_extra,
@@ -218,7 +246,10 @@ class PageEditView(LoginRequiredMixin,UserPassesTestMixin,FormView):
raise PermissionDenied()
self.instance = instance
return super().get(request)
return render(request,
self.get_template_name(),
self.get_context_data(page=instance))
def post(self,request,slug:str):
instance = get_object_or_404(Page,slug=slug)
@@ -255,7 +286,138 @@ class PageEditView(LoginRequiredMixin,UserPassesTestMixin,FormView):
self.get_template_name(),
self.get_context_data(slug_invalid=True))
class PageDeleteView(View):
template_name = "tinywiki/page/delete"
def get(self,request,slug):
return render()
class PageDeleteView(LoginRequiredMixin,UserPassesTestMixin,FormView):
template_name = "tinywiki/page/delete.html"
form_class = PageDeleteForm
def test_func(self) -> bool:
if self.request.user.is_staff:
return True
if self.request.user.has_perm('tinyiwki-delete-all') or self.request.user.has_perm('tinywiki-delete'):
return True
return False
def get(self,request:HttpRequest,slug:str)->HttpResponse:
instance = get_object_or_404(Page,slug=slug)
if not request.user.is_staff and not instance.author == request.user:
raise PermissionDenied()
return render(request,
self.template_name,
self.get_context_data(form=self.get_form_class()(),
page=instance))
def form_invalid(self,form:PageDeleteForm)->HttpResponse:
return redirect(reverse('tinywiki:page',kwargs={'slug':self.instance.slug}))
def form_valid(self,form:PageDeleteForm)->HttpResponse:
if self.instance.slug == form.cleaned_data['slug']:
try:
self.instance.delete()
return redirect(reverse("tinywiki:home"))
except: pass
return redirect(reverse('tinywiki:page',kwargs={'slug':self.instance.slug}))
def post(self,request:HttpRequest,slug:str)->HttpResponse:
self.instance = get_object_or_404(Page,slug=slug)
if not request.user.is_staff and not instance.author == request.user:
raise PermissionDenied()
return super().post(request)
class HxPageEditView(LoginRequiredMixin,UserPassesTestMixin,FormView):
template_name = "tinywiki/page/hx-edit.html"
bs_template_name = "tinywiki/page/hx-bs-edit.html"
form_class = PageForm
def test_func(self) -> bool:
if self.request.user.is_authenticated:
if self.request.user.is_staff:
return True
if self.request.user.has_perm('tinywiki-edit-page') or self.request.user.has_perm('tinywiki-edit-all-pages'):
return True
return False
def get_template_name(self) -> str:
if settings.USE_BOOTSTRAP:
return self.bs_template_name
return self.template_name
def get_context_data(self,**kwargs):
context = super().get_context_data(**kwargs)
context['create']=False
context['page']=self.instance
context.setdefault("title_extra","")
context.setdefault("slug_extra","")
context.setdefault("content_type_extra","")
context.setdefault("status_extra","")
context.setdefault("content_extra","")
return context
def form_invalid(self, form):
if 'slug' in form.errors:
slug_extra = "is-invalid"
else:
slug_extra = "is-valid"
if 'title' in form.errors:
title_extra = 'is-invalid'
else:
title_extra = 'is-valid'
if 'status_data' in form.errors:
status_extra = 'is-invalid'
else:
status_extra = 'is-valid'
if 'content_type_data' in form.errors:
content_type_extra = 'is-invalid'
else:
content_type_extra = 'is-valid'
if 'content' in form.errors:
content_extra = 'is-invalid'
else:
content_extra = 'is-valid'
return render(self.request,
self.get_template_name(),
self.get_context_data(
page=self.instance,
))
def post(self,request,pk:int):
instance = get_object_or_404(Page,pk=pk)
user = request.user
if (instance.slug.startswith('tw-') and not user.is_staff):
raise PermissionDenied(_("Only staff users are allowed to edit TinyWiki system pages!"))
if user.pk != instance.author.pk:
if not user.is_staff and not user.has_perm("page.tinywiki-edit-all"):
raise PermissionDenied()
else:
if not user.has_perm('page.tinywiki-edit-all') or not user.has_perm('page.tinywiki-edit'):
raise PermissionDenied()
self.instance = instance
return super().post(request)
def get_form(self):
return self.get_form_class()(instance=self.instance,**self.get_form_kwargs())
def form_valid(self,form):
user = self.request.user
instance = form.save(commit=False)
instance.created_by = user
instance.last_edited_by = user
try:
form.save(commit=True)
return render(self.request,self.get_template_name(),self.get_context_data(save_success=True))
except:
return render(self.request,
self.get_template_name(),
self.get_context_data(save_success=False))
class HxPageDeleteView(View):
pass