2025.09.17-19:07:57
@@ -21,6 +21,6 @@ COPY . .
|
|||||||
|
|
||||||
RUN poetry install --all-groups \
|
RUN poetry install --all-groups \
|
||||||
&& chmod 0755 start-django.sh
|
&& chmod 0755 start-django.sh
|
||||||
|
HEALTHCHECK --interval=30s --retries=5 --timeout=30s CMD curl -sS 127.0.0.1:8000
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
ENTRYPOINT ["/app/start-django.sh"]
|
ENTRYPOINT ["/app/start-django.sh"]
|
||||||
|
|||||||
22
LICENSE
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Copyright 2025 Christian Moser
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification,are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -11,13 +11,20 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from environ import Env
|
from environ import Env
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
LOCAL_DIR = Path(__file__).resolve().parent / "local"
|
||||||
|
|
||||||
ENV = Env(
|
ENV = Env(
|
||||||
DEBUG=(bool,False),
|
DEBUG=(bool,False),
|
||||||
|
DOTENV=(Path,LOCAL_DIR/'.env'),
|
||||||
|
DOTENV_PROD=(Path,LOCAL_DIR/'.env.prod'),
|
||||||
|
DOTENV_DEVEL=(Path,LOCAL_DIR/'.env.dev'),
|
||||||
DATABASE_URL=(str,f"sqlite:///{str(BASE_DIR/"db.sqlite3").replace("\\","/")}"),
|
DATABASE_URL=(str,f"sqlite:///{str(BASE_DIR/"db.sqlite3").replace("\\","/")}"),
|
||||||
ALLOWED_HOSTS=(list,['*']),
|
ALLOWED_HOSTS=(list,['*']),
|
||||||
STATIC_URL=(str,"static/"),
|
STATIC_URL=(str,"static/"),
|
||||||
@@ -28,15 +35,18 @@ ENV = Env(
|
|||||||
EMAIL_BACKEND=(str,"console"),
|
EMAIL_BACKEND=(str,"console"),
|
||||||
)
|
)
|
||||||
|
|
||||||
DEBUG = ENV.bool("DEBUG")
|
_env_file = Path(ENV.path("DOTENV"))
|
||||||
|
if _env_file.is_file():
|
||||||
|
ENV.read_env(_env_file)
|
||||||
|
|
||||||
DEBUG = ENV.bool("DEBUG")
|
DEBUG = ENV.bool("DEBUG")
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
_env_file = Path(ENV.path("DOTENV",str(BASE_DIR/'.env.dev'))).resolve()
|
_env_file = Path(ENV.path("DOTENV_DEVEL")).resolve()
|
||||||
if _env_file.is_file():
|
if _env_file.is_file():
|
||||||
ENV.read_env(_env_file)
|
ENV.read_env(_env_file)
|
||||||
else:
|
else:
|
||||||
_env_file = Path(ENV.path("DOTENV",str(BASE_DIR/'.env.prod'))).resolve()
|
_env_file = Path(ENV.path("DOTENV_PROD")).resolve()
|
||||||
if _env_file.is_file():
|
if _env_file.is_file():
|
||||||
ENV.read_env(_env_file)
|
ENV.read_env(_env_file)
|
||||||
del _env_file
|
del _env_file
|
||||||
@@ -119,7 +129,7 @@ ROOT_URLCONF = 'django_project.urls'
|
|||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [],
|
'DIRS': [BASE_DIR/"templates"],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
@@ -174,6 +184,8 @@ USE_I18N = True
|
|||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
# Auth settings
|
||||||
|
LOGIN_REDIRECT_URL = reverse_lazy('tinywiki:home')
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||||
@@ -186,6 +198,9 @@ MEDIA_ROOT = ENV("MEDIA_ROOT")
|
|||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
BASE_DIR/"static",
|
||||||
|
]
|
||||||
|
|
||||||
if ENV("EMAIL_BACKEND") == 'smtp':
|
if ENV("EMAIL_BACKEND") == 'smtp':
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||||
@@ -201,4 +216,24 @@ else:
|
|||||||
print("Email backend not known falling back to console!",file=sys.stderr)
|
print("Email backend not known falling back to console!",file=sys.stderr)
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||||
|
|
||||||
|
_secret_key = LOCAL_DIR / "secret_key.py"
|
||||||
|
if _secret_key.is_file():
|
||||||
|
from .local import secret_key
|
||||||
|
if hasattr(secret_key,'SECRET_KEY'):
|
||||||
|
SECRET_KEY = secret_key.SECRET_KEY
|
||||||
|
if hasattr(secret_key,"SECRET_KEY_FALLBACKS"):
|
||||||
|
SECRET_KEY_FALLBACKS = secret_key.SECRET_KEY_FALLBACKS
|
||||||
|
|
||||||
|
_local_settings = LOCAL_DIR / "settings.py"
|
||||||
|
if _local_settings.is_file():
|
||||||
|
from .local.settings import *
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
_local_settings = LOCAL_DIR / "settings_dev.py"
|
||||||
|
if _local_settings.is_file():
|
||||||
|
from .local.settings_dev import *
|
||||||
|
else:
|
||||||
|
_local_settings = LOCAL_DIR / "settings_prod.py"
|
||||||
|
if _local_settings.is_file():
|
||||||
|
from .local.settings_prod import *
|
||||||
|
del _local_settings
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ urlpatterns = [
|
|||||||
path('',include("tinywiki.urls")),
|
path('',include("tinywiki.urls")),
|
||||||
path("user/",include("user.urls")),
|
path("user/",include("user.urls")),
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
path('accounts/',include('allauth.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|||||||
59
package-lock.json
generated
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "tinywiki",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"bootstrap": "^5.3.8",
|
||||||
|
"bootstrap-icons": "^1.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@popperjs/core": {
|
||||||
|
"version": "2.11.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
|
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/popperjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bootstrap": {
|
||||||
|
"version": "5.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
|
||||||
|
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/twbs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/bootstrap"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@popperjs/core": "^2.11.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bootstrap-icons": {
|
||||||
|
"version": "1.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz",
|
||||||
|
"integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/twbs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/bootstrap"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
package.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"bootstrap": "^5.3.8",
|
||||||
|
"bootstrap-icons": "^1.13.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
static/icons/bootstrap-icons.svg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
90
templates/base.html
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
{% load i18n static %}
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<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 %}
|
||||||
|
<script>
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'));
|
||||||
|
</script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<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">
|
||||||
|
{% if brand_logo %}
|
||||||
|
<img class="mb2 me-2" width="46" height="46" src="{{ brand_logo }}">
|
||||||
|
{% else %}
|
||||||
|
<svg class="bi mb-2 me-2 text-white" width="46" height="46" fill="currentColor">
|
||||||
|
<use xlink:href="{% static 'icons/bootstrap-icons.svg' %}#book" ></use>
|
||||||
|
</svg>
|
||||||
|
{% endif %}
|
||||||
|
<span class="display-6 font-weight-bold text-white me-2">{{ brand_name }}</span>
|
||||||
|
{% if subtitle %}
|
||||||
|
<span class="h2">{{ subtitle }}</h2>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="navbar navbar-expand-lg">
|
||||||
|
<div class="collapse navbar-collapse">
|
||||||
|
<ul class="navbar-nav mb-2 mb-lg-0 me-auto">
|
||||||
|
<li class="nav-item me-3">
|
||||||
|
<a class="nav-link" href="{% url 'tinywiki:home' %}">HOME</a>
|
||||||
|
</li>
|
||||||
|
{% if user_can_create_wiki_pages %}
|
||||||
|
<li class="nav-item me-3">
|
||||||
|
<a class="nav-link" href="{% url 'tinywiki:page-create' %}">{% translate "Create Page" %}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="nav-item me-3">
|
||||||
|
<a class="nav-link" href="{% url 'tinywiki:toc' %}">{% translate "Wiki content" %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav mb-2 mb-lg-0 list-group-horizontal">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<form method="POST" action="{% url 'account_logout' %}" class="d-inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="nav-link">{% translate "Log out" %}</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url "account_login" %}">{% translate "Log in" %}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row h-100">
|
||||||
|
<div class="col-lg-3">
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<main>
|
||||||
|
{% block content %}
|
||||||
|
<h1>It Works!</h1>
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-3">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="bg-secondary text-white mt-auto">
|
||||||
|
<span class="ms-2">
|
||||||
|
Powered by TinyWiki
|
||||||
|
</span>
|
||||||
|
<span class="text-end">
|
||||||
|
<a class="text-white" href="{% url 'tinywiki:page' 'tw-license' %}"> © 2025</a>
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
<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 %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -35,8 +35,52 @@ class WikiContentType(StrEnum):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__qualname__}: {self.value.upper()}>"
|
return f"<{self.__qualname__}: {self.value.upper()}>"
|
||||||
|
|
||||||
|
|
||||||
WIKI_CONTENT_TYPES = (
|
WIKI_CONTENT_TYPES = (
|
||||||
WikiContentType.MARKDOWN,
|
WikiContentType.MARKDOWN,
|
||||||
WikiContentType.BBCODE,
|
WikiContentType.BBCODE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class WikiPageStatus(StrEnum):
|
||||||
|
IN_PROGRESS = "in_progress"
|
||||||
|
DRAFT = "draft"
|
||||||
|
PUBLISHED = "published"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_string(string:str)->"WikiPageStatus":
|
||||||
|
mapping = {
|
||||||
|
WikiPageStatus.IN_PROGRESS.value: WikiPageStatus.IN_PROGRESS,
|
||||||
|
WikiPageStatus.DRAFT.value: WikiPageStatus.DRAFT,
|
||||||
|
WikiPageStatus.PUBLISHED: WikiPageStatus.PUBLISHED,
|
||||||
|
}
|
||||||
|
return mapping[string.lower()]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def str_raw(self)->str:
|
||||||
|
mapping = {
|
||||||
|
WikiPageStatus.IN_PROGRESS: _("in progress"),
|
||||||
|
WikiPageStatus.DRAFT: _("draft"),
|
||||||
|
WikiPageStatus.PUBLISHED: _("published"),
|
||||||
|
}
|
||||||
|
return mapping[self]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def str_lazy(self)->str:
|
||||||
|
return gettext_lazy(self.str_raw)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def str(self)->str:
|
||||||
|
return gettext(self.str_raw)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.str
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<{self.__qualname__}: {self.value.upper()}>"
|
||||||
|
|
||||||
|
WIKI_PAGE_STATUS = (
|
||||||
|
WikiPageStatus.IN_PROGRESS,
|
||||||
|
WikiPageStatus.DRAFT,
|
||||||
|
WikiPageStatus.PUBLISHED,
|
||||||
|
)
|
||||||
26
tinywiki/forms.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from django import forms
|
||||||
|
from .models import Page,Image
|
||||||
|
|
||||||
|
class PageForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Page
|
||||||
|
fields = [
|
||||||
|
'title',
|
||||||
|
'slug',
|
||||||
|
'status_data',
|
||||||
|
'content_type_data',
|
||||||
|
'content',
|
||||||
|
]
|
||||||
|
|
||||||
|
class PageAdminForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Page
|
||||||
|
fields = [
|
||||||
|
'title',
|
||||||
|
'author',
|
||||||
|
'slug',
|
||||||
|
'status_data',
|
||||||
|
'content_type_data',
|
||||||
|
'content',
|
||||||
|
]
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# Generated by Django 5.2.4 on 2025-09-14 13:31
|
# Generated by Django 5.2.4 on 2025-09-15 00:26
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
import tinywiki.models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@@ -22,8 +23,8 @@ class Migration(migrations.Migration):
|
|||||||
('alt', models.CharField(max_length=511, verbose_name='alternative text')),
|
('alt', models.CharField(max_length=511, verbose_name='alternative text')),
|
||||||
('description', models.CharField(blank=True, max_length=1023, null=True, verbose_name='description')),
|
('description', models.CharField(blank=True, max_length=1023, null=True, verbose_name='description')),
|
||||||
('image', models.ImageField(upload_to='tinywiki/img', verbose_name='image file')),
|
('image', models.ImageField(upload_to='tinywiki/img', verbose_name='image file')),
|
||||||
('uploaded_at', models.DateTimeField(auto_now_add=True, verbose_name='upladed at')),
|
('uploaded_at', models.DateTimeField(auto_now_add=True, verbose_name='uploaded at')),
|
||||||
('uploaded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tinywiki_image_uploads', to=settings.AUTH_USER_MODEL, verbose_name='uploaded by')),
|
('uploaded_by', models.ForeignKey(default=tinywiki.models.get_tinywiki_default_user, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tinywiki_image_uploads', to=settings.AUTH_USER_MODEL, verbose_name='uploaded by')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
@@ -32,13 +33,14 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('slug', models.SlugField(max_length=255, unique=True, verbose_name='slug')),
|
('slug', models.SlugField(max_length=255, unique=True, verbose_name='slug')),
|
||||||
('title', models.CharField(max_length=255, verbose_name='title')),
|
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||||
|
('status_data', models.CharField(default='', max_length=15, verbose_name='status')),
|
||||||
('content_type_data', models.CharField(choices=[('markdown', 'Markdown'), ('bbcode', 'BBCode')], default='bbcode', verbose_name='content type')),
|
('content_type_data', models.CharField(choices=[('markdown', 'Markdown'), ('bbcode', 'BBCode')], default='bbcode', verbose_name='content type')),
|
||||||
('content', models.TextField(verbose_name='Page content')),
|
('content', models.TextField(verbose_name='Page content')),
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
|
||||||
('last_edited_at', models.DateTimeField(auto_now=True, verbose_name='last edited at')),
|
('last_edited_at', models.DateTimeField(auto_now=True, verbose_name='last edited at')),
|
||||||
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tinywiki_athors', to=settings.AUTH_USER_MODEL, verbose_name='author')),
|
('author', models.ForeignKey(default=tinywiki.models.get_tinywiki_default_user, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tinywiki_athors', to=settings.AUTH_USER_MODEL, verbose_name='author')),
|
||||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tinywiki_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
|
('created_by', models.ForeignKey(default=tinywiki.models.get_tinywiki_default_user, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tinywiki_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
|
||||||
('last_edited_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tinywiki_last_edited', to=settings.AUTH_USER_MODEL, verbose_name='last edited by')),
|
('last_edited_by', models.ForeignKey(default=tinywiki.models.get_tinywiki_default_user, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='tinywiki_last_edited', to=settings.AUTH_USER_MODEL, verbose_name='last edited by')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,18 +9,90 @@ class Migration(migrations.Migration):
|
|||||||
|
|
||||||
def init_tinywiki_user(apps,schema_editor):
|
def init_tinywiki_user(apps,schema_editor):
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
user = get_user_model.objects.create_user(**settings.TINYWIKI_USER_CONFIG)
|
user = get_user_model().objects.create_user(**settings.TINYWIKI_USER_CONFIG)
|
||||||
|
|
||||||
|
def init_tinywiki_groups_and_permissions(apps,schema_editor):
|
||||||
|
from ..models import Page
|
||||||
|
from django.contrib.auth.models import Group,Permission
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
PERMISSIONS = [
|
||||||
|
'tinywiki-read-all',
|
||||||
|
'tinywiki-delete',
|
||||||
|
'tinywiki-create',
|
||||||
|
'tinywiki-create-system',
|
||||||
|
'tinywiki-edit',
|
||||||
|
'tinywiki-edit-system',
|
||||||
|
'tinywiki-edit-all',
|
||||||
|
'tinywiki-delete-all',
|
||||||
|
'tinywiki-delete-system',
|
||||||
|
]
|
||||||
|
|
||||||
|
GROUPS = [
|
||||||
|
('tinywiki-moderator',('tinywiki-read-all',
|
||||||
|
'tinywiki-delete-all',
|
||||||
|
'tinywiki-edit-all',
|
||||||
|
'tinywiki-create')),
|
||||||
|
('tinywiki-author',('tinywiki-create',
|
||||||
|
'tinywiki-edit',
|
||||||
|
'tinywiki-delete')),
|
||||||
|
('tinywiki-reader',('tinywiki-read-all',)),
|
||||||
|
('tinywiki-admin',('tinywiki-read-all',
|
||||||
|
'tinywiki-create',
|
||||||
|
'tinywiki-create-system',
|
||||||
|
'tinywiki-delete-all',
|
||||||
|
'tinywiki-delete-system',
|
||||||
|
'tinywiki-edit-all',
|
||||||
|
'tinywiki-edit-system'))
|
||||||
|
]
|
||||||
|
|
||||||
|
perm_mapping = {}
|
||||||
|
content_type = ContentType.objects.get_for_model(Page)
|
||||||
|
for perm in PERMISSIONS:
|
||||||
|
permission = Permission.objects.create(codename=perm,content_type=content_type)
|
||||||
|
perm_mapping[perm] = permission
|
||||||
|
|
||||||
|
|
||||||
|
for grp,perms in GROUPS:
|
||||||
|
group = Group.objects.create(name=grp)
|
||||||
|
for perm in perms:
|
||||||
|
group.permissions.add(perm_mapping[perm])
|
||||||
|
|
||||||
|
|
||||||
def init_default_pages(apps,schema_editor)->None:
|
def init_default_pages(apps,schema_editor)->None:
|
||||||
from ..models import Page,Image
|
from ..models import Page,Image
|
||||||
#TODO
|
from ..enums import WikiContentType,WikiPageStatus
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
|
||||||
|
page_path = Path(__file__).resolve().parent / "pages"
|
||||||
|
json_file = page_path / "pages.json"
|
||||||
|
if json_file.is_file():
|
||||||
|
with open(json_file,"rt",encoding="utf-8") as ifile:
|
||||||
|
data=json.loads(ifile.read())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for slug,spec in data.items():
|
||||||
|
filename = page_path / spec['file']
|
||||||
|
with open(filename,"rt",encoding="utf-8") as ifile:
|
||||||
|
content = ifile.read()
|
||||||
|
|
||||||
|
Page.objects.create(slug=slug,
|
||||||
|
title=spec['title'],
|
||||||
|
status_data=WikiPageStatus.from_string(spec['status']).value,
|
||||||
|
content_type_data=WikiContentType.from_string(spec['content_type']).value,
|
||||||
|
content=content)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def init_user_pages(apps,schema_edit)->None:
|
def init_user_pages(apps,schema_edit)->None:
|
||||||
from ..models import Page,Image
|
from ..models import Page,Image
|
||||||
#TODO
|
#TODO
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.RunPython(init_tinywiki_groups_and_permissions),
|
||||||
migrations.RunPython(init_tinywiki_user),
|
migrations.RunPython(init_tinywiki_user),
|
||||||
migrations.RunPython(init_default_pages)
|
migrations.RunPython(init_default_pages),
|
||||||
]
|
]
|
||||||
|
|
||||||
23
tinywiki/migrations/pages/bs-license.bbcode
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[h2]The MIT License (MIT)[/h2]
|
||||||
|
|
||||||
|
[h3]Copyright [copy] 2011-2025 The Bootstrap Authors[/h3]
|
||||||
|
|
||||||
|
[p]Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
[ul][li]
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.[/li][/ul]
|
||||||
|
[/p]
|
||||||
|
|
||||||
|
[p][b]THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.[/b][/p]
|
||||||
10
tinywiki/migrations/pages/license.bbcode
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[h2]Copyright [copy] 2025 Christian Moser[/h2]
|
||||||
|
|
||||||
|
[p]Redistribution and use in source and binary forms, with or without modification,are permitted provided that the following conditions are met:[/p]
|
||||||
|
|
||||||
|
[ol]
|
||||||
|
[li]Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.[/li]
|
||||||
|
[li]Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.[/li]
|
||||||
|
[/ol]
|
||||||
|
|
||||||
|
[p][b]THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.[/b][/p]
|
||||||
14
tinywiki/migrations/pages/pages.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"tw-license": {
|
||||||
|
"title": "TinyWiki License",
|
||||||
|
"content_type":"bbcode",
|
||||||
|
"status":"published",
|
||||||
|
"file":"license.bbcode"
|
||||||
|
},
|
||||||
|
"tw-bootstrap-license": {
|
||||||
|
"title": "Bootstrap License",
|
||||||
|
"content_type": "bbcode",
|
||||||
|
"status":"published",
|
||||||
|
"file":"bs-license.bbcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,21 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils.safestring import mark_safe,SafeText
|
from django.utils.safestring import mark_safe,SafeText
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from tinywiki.enums import WIKI_CONTENT_TYPES, WikiContentType
|
from tinywiki.enums import WIKI_CONTENT_TYPES, WIKI_PAGE_STATUS, WikiContentType, WikiPageStatus
|
||||||
import markdown
|
import markdown
|
||||||
import bbcode
|
from .parser import parse_bbcode
|
||||||
|
from . import settings
|
||||||
|
import tinywiki
|
||||||
|
|
||||||
|
def get_tinywiki_default_user():
|
||||||
|
UserModel = get_user_model()
|
||||||
|
try:
|
||||||
|
user = UserModel.objects.get(**settings.TINYWIKI_USER_LOOKUP)
|
||||||
|
except UserModel.DoesNotExist:
|
||||||
|
user = UserModel.objects.filter(is_superuser=True).order_by('pk')[0]
|
||||||
|
return user
|
||||||
|
|
||||||
class Page(models.Model):
|
class Page(models.Model):
|
||||||
slug = models.SlugField(_("slug"),
|
slug = models.SlugField(_("slug"),
|
||||||
@@ -20,11 +31,16 @@ class Page(models.Model):
|
|||||||
null=False,
|
null=False,
|
||||||
blank=False)
|
blank=False)
|
||||||
author = models.ForeignKey(get_user_model(),
|
author = models.ForeignKey(get_user_model(),
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_DEFAULT,
|
||||||
|
default=get_tinywiki_default_user,
|
||||||
verbose_name=_("author"),
|
verbose_name=_("author"),
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
related_name="tinywiki_athors")
|
related_name="tinywiki_athors")
|
||||||
|
status_data = models.CharField(_("status"),
|
||||||
|
choices=[(i.value,i.str_lazy) for i in WIKI_PAGE_STATUS],
|
||||||
|
max_length=15,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
default=WikiPageStatus.IN_PROGRESS.value)
|
||||||
content_type_data = models.CharField(_("content type"),
|
content_type_data = models.CharField(_("content type"),
|
||||||
choices=[(i.value,i.str_lazy) for i in WIKI_CONTENT_TYPES],
|
choices=[(i.value,i.str_lazy) for i in WIKI_CONTENT_TYPES],
|
||||||
default=WikiContentType.BBCODE.value)
|
default=WikiContentType.BBCODE.value)
|
||||||
@@ -37,31 +53,49 @@ class Page(models.Model):
|
|||||||
created_at = models.DateTimeField(_("created at"),
|
created_at = models.DateTimeField(_("created at"),
|
||||||
auto_now_add=True)
|
auto_now_add=True)
|
||||||
created_by = models.ForeignKey(get_user_model(),
|
created_by = models.ForeignKey(get_user_model(),
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_DEFAULT,
|
||||||
verbose_name=_("created by"),
|
verbose_name=_("created by"),
|
||||||
null=True,
|
default=get_tinywiki_default_user,
|
||||||
blank=True,
|
|
||||||
related_name="tinywiki_created")
|
related_name="tinywiki_created")
|
||||||
|
|
||||||
last_edited_at = models.DateTimeField(_("last edited at"),
|
last_edited_at = models.DateTimeField(_("last edited at"),
|
||||||
auto_now=True)
|
auto_now=True)
|
||||||
last_edited_by = models.ForeignKey(get_user_model(),
|
last_edited_by = models.ForeignKey(get_user_model(),
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_DEFAULT,
|
||||||
verbose_name=_("last edited by"),
|
verbose_name=_("last edited by"),
|
||||||
null=True,
|
default=get_tinywiki_default_user,
|
||||||
blank=True,
|
|
||||||
related_name="tinywiki_last_edited")
|
related_name="tinywiki_last_edited")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def content_type(self)->WikiContentType:
|
def content_type(self)->WikiContentType:
|
||||||
return WikiContentType.from_string(self.content_type_data)
|
return WikiContentType.from_string(self.content_type_data)
|
||||||
|
@content_type.setter
|
||||||
|
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):
|
||||||
|
self.content_type_data = content_type.value
|
||||||
|
else:
|
||||||
|
raise TypeError("content_type")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self)->WikiPageStatus:
|
||||||
|
return WikiPageStatus.from_string(self.status_data)
|
||||||
|
@status.setter
|
||||||
|
def status(self,status:str|WikiPageStatus):
|
||||||
|
if isinstance(status,str):
|
||||||
|
self.status_data = WikiPageStatus.from_string(status).value
|
||||||
|
elif isinstance(status,WikiPageStatus):
|
||||||
|
self.status_data = status.value
|
||||||
|
else:
|
||||||
|
raise TypeError("status")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def html_content(self)->SafeText|str:
|
def html_content(self)->SafeText|str:
|
||||||
if self.content_type == WikiContentType.MARKDOWN:
|
if self.content_type == WikiContentType.MARKDOWN:
|
||||||
return mark_safe(markdown.markdown(self.content))
|
return mark_safe(markdown.markdown(self.content))
|
||||||
elif self.content_type == WikiContentType.BBCODE:
|
elif self.content_type == WikiContentType.BBCODE:
|
||||||
return mark_safe(bbcode.render_html(self.content))
|
return mark_safe(parse_bbcode(self.content))
|
||||||
return self.content
|
return self.content
|
||||||
|
|
||||||
|
|
||||||
@@ -81,10 +115,9 @@ class Image(models.Model):
|
|||||||
upload_to="tinywiki/img")
|
upload_to="tinywiki/img")
|
||||||
|
|
||||||
uploaded_by = models.ForeignKey(get_user_model(),
|
uploaded_by = models.ForeignKey(get_user_model(),
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_DEFAULT,
|
||||||
verbose_name=_("uploaded by"),
|
verbose_name=_("uploaded by"),
|
||||||
null=True,
|
default=get_tinywiki_default_user,
|
||||||
blank=True,
|
|
||||||
related_name="tinywiki_image_uploads")
|
related_name="tinywiki_image_uploads")
|
||||||
uploaded_at = models.DateTimeField(_("uploaded at"),
|
uploaded_at = models.DateTimeField(_("uploaded at"),
|
||||||
auto_now_add=True)
|
auto_now_add=True)
|
||||||
|
|||||||
4
tinywiki/parser/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .bbcode import PARSER as BBCODE_PARSER
|
||||||
|
|
||||||
|
def parse_bbcode(text:str):
|
||||||
|
return BBCODE_PARSER.format(text)
|
||||||
28
tinywiki/parser/bbcode/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import bbcode
|
||||||
|
from . import formatters
|
||||||
|
PARSER = bbcode.Parser(newline="\n",escape_html=True)
|
||||||
|
|
||||||
|
def _():
|
||||||
|
for i in formatters.SIMPLE_FORMATTERS:
|
||||||
|
if len(i) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(i) == 1:
|
||||||
|
kwargs = {}
|
||||||
|
else:
|
||||||
|
kwargs = i[1]
|
||||||
|
PARSER.add_simple_formatter(*i[0],**kwargs)
|
||||||
|
|
||||||
|
for i in formatters.FORMATTERS:
|
||||||
|
if len(i) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(i) == 1:
|
||||||
|
kwargs = {}
|
||||||
|
else:
|
||||||
|
kwargs = i[1]
|
||||||
|
|
||||||
|
PARSER.add_formatter(*i[0],**kwargs)
|
||||||
|
|
||||||
|
_()
|
||||||
|
del _
|
||||||
35
tinywiki/parser/bbcode/formatters.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
from .text_formatters import (
|
||||||
|
render_codeblock,
|
||||||
|
render_url,
|
||||||
|
render_list_item,
|
||||||
|
render_ordered_list,
|
||||||
|
render_unordered_list,
|
||||||
|
render_paragraph,
|
||||||
|
render_image,
|
||||||
|
render_wiki_image,
|
||||||
|
render_wiki_link,
|
||||||
|
render_wiki_url,
|
||||||
|
)
|
||||||
|
from .simple_formatters import (
|
||||||
|
SIMPLE_HEADER_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}),
|
||||||
|
(('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})
|
||||||
|
]
|
||||||
12
tinywiki/parser/bbcode/simple_formatters.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
SIMPLE_HEADER_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>"),{}),
|
||||||
|
(('copy',"©"),{'standalone':True}),
|
||||||
|
(('reg',"®"),{'standalone':True}),
|
||||||
|
(('trade',"™"),{'standalone':True}),
|
||||||
|
]
|
||||||
221
tinywiki/parser/bbcode/text_formatters.py
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def render_url(tag_name:str,value,options,parent,context):
|
||||||
|
try:
|
||||||
|
url = options['url']
|
||||||
|
except KeyError:
|
||||||
|
url = value
|
||||||
|
|
||||||
|
if '://' 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>"
|
||||||
|
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']
|
||||||
|
try:
|
||||||
|
page = models.Page.objects.get(slug=slug)
|
||||||
|
except models.Page.DoesNotExist:
|
||||||
|
page = None
|
||||||
|
|
||||||
|
else:
|
||||||
|
url = reverse('tinywiki:home')
|
||||||
|
slug=None
|
||||||
|
|
||||||
|
if settings.USE_BOOTSTRAP:
|
||||||
|
if page:
|
||||||
|
if page.slug.startswith('tw-'):
|
||||||
|
svg=render_to_string('tinywiki/icons/journal.svg')
|
||||||
|
elif page.slug:
|
||||||
|
svg=render_to_string('tinywiki/icons/book.svg')
|
||||||
|
else:
|
||||||
|
svg=render_to_string('tinywiki/icons/file-earmark-x')
|
||||||
|
|
||||||
|
return f"<a href=\"{url}\" class=\"icon-link icon-link-hover\">{value}{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']
|
||||||
|
try:
|
||||||
|
page = models.Page.objects.get(slug=slug)
|
||||||
|
title = page.title
|
||||||
|
if slug.starts_with('tw-'):
|
||||||
|
svg = "tinywiki/icons/journal.svg"
|
||||||
|
else:
|
||||||
|
svg = "tinywiki/icons/book.svg"
|
||||||
|
except:
|
||||||
|
page = None
|
||||||
|
title = _("Page not found")
|
||||||
|
svg_template = "tinywiki/icons/file-earmark-x.svg"
|
||||||
|
url = reverse("tinywiki:page",kwargs={'slug':slug})
|
||||||
|
else:
|
||||||
|
slug = None
|
||||||
|
title = _("Home")
|
||||||
|
url = reverse("tinywiki:home")
|
||||||
|
svg_template = "tinywiki/icons/house.svg"
|
||||||
|
|
||||||
|
if settings.USE_BOOTSTRAP:
|
||||||
|
return f"<a href=\"{url}\" class=\"icon-link icon-link-hover\">{value}{render_to_string(svg_template)}</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>"
|
||||||
|
|
||||||
|
def render_ordered_list(tag_name:str,value,options,parent,context)->str:
|
||||||
|
return f"<ol>{value}</ol>"
|
||||||
|
|
||||||
|
def render_unordered_list(tag_name:str,value,options,parent,context)->str:
|
||||||
|
return f"<ul>{value}</ul>"
|
||||||
|
|
||||||
|
def render_list_item(tag_name:str,value,options,parent,context)->str:
|
||||||
|
return f"<li>{value}</li>"
|
||||||
|
|
||||||
|
|
||||||
|
def render_paragraph(tag_name:str,value,options,parent,context):
|
||||||
|
if settings.USE_BOOTSTRAP:
|
||||||
|
return f"<p style=\"text-align:justify;\">{value}</p>"
|
||||||
|
return f"<p>{value}</p>"
|
||||||
|
|
||||||
|
def render_image(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:
|
||||||
|
classes=["img-fluid","figure-img","rounded"]
|
||||||
|
fig_classes=["figure","my-1"]
|
||||||
|
styles=[]
|
||||||
|
fig_styles=[]
|
||||||
|
else:
|
||||||
|
styles=["max-width:100%;"]
|
||||||
|
classes=[]
|
||||||
|
fig_classes=[]
|
||||||
|
fig_styles=[]
|
||||||
|
|
||||||
|
if 'width' in options:
|
||||||
|
_w = options['width']
|
||||||
|
if _w.endswith('px'):
|
||||||
|
fig_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)
|
||||||
|
fig_classes.append(f'w-{width}')
|
||||||
|
else:
|
||||||
|
fig_styles.append(f"width:{_w}%;")
|
||||||
|
|
||||||
|
if "position" in options:
|
||||||
|
pos = options['position']
|
||||||
|
if settings.USE_BOOTSTRAP:
|
||||||
|
if pos == "left" or pos=="start":
|
||||||
|
fig_classes += ["float-start","me-2"]
|
||||||
|
elif pos == "right" or pos == "end":
|
||||||
|
fig_classes += ["float-end","ms-2"]
|
||||||
|
elif pos == "center":
|
||||||
|
fig_classes += ["mx-auto","d-block"]
|
||||||
|
|
||||||
|
if styles:
|
||||||
|
style=f"style=\"{"".join(styles)}\""
|
||||||
|
else:
|
||||||
|
style=""
|
||||||
|
|
||||||
|
if fig_styles:
|
||||||
|
fig_style=f'style="{"".join(fig_styles)}"'
|
||||||
|
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>'
|
||||||
|
else:
|
||||||
|
return f'<figure {fig_style}><img src="{options[tag_name]}" {style}><figcaption>{value}</figcaption></figure>'
|
||||||
|
|
||||||
|
def render_wiki_image(tag_name:str,value,options,parent,context):
|
||||||
|
if tag_name not in options:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
image = models.Image.objects.get(slug=options[tag_name])
|
||||||
|
except models.Image.DoesNotExist:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if settings.USE_BOOTSTRAP:
|
||||||
|
classes=["img-fluid","figure-img","rounded"]
|
||||||
|
fig_classes=["figure","my-1"]
|
||||||
|
styles=[]
|
||||||
|
fig_styles=[]
|
||||||
|
else:
|
||||||
|
styles=["max-width:100%;"]
|
||||||
|
classes=[]
|
||||||
|
fig_classes=[]
|
||||||
|
fig_styles=[]
|
||||||
|
|
||||||
|
if 'width' in options:
|
||||||
|
_w = options['width']
|
||||||
|
if _w.endswith('px'):
|
||||||
|
fig_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)
|
||||||
|
fig_classes.append(f'w-{width}')
|
||||||
|
else:
|
||||||
|
fig_styles.append(f"width:{_w}%;")
|
||||||
|
|
||||||
|
if "position" in options:
|
||||||
|
pos = options['position']
|
||||||
|
if settings.USE_BOOTSTRAP:
|
||||||
|
if pos == "left" or pos=="start":
|
||||||
|
fig_classes += ["float-start","me-2"]
|
||||||
|
elif pos == "right" or pos == "end":
|
||||||
|
fig_classes += ["float-end","ms-2"]
|
||||||
|
elif pos == "center":
|
||||||
|
fig_classes += ["mx-auto","d-block"]
|
||||||
|
|
||||||
|
if styles:
|
||||||
|
style=f"style=\"{"".join(styles)}\""
|
||||||
|
else:
|
||||||
|
style=""
|
||||||
|
|
||||||
|
if fig_styles:
|
||||||
|
fig_style=f'style="{"".join(fig_styles)}"'
|
||||||
|
else:
|
||||||
|
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>'
|
||||||
|
else:
|
||||||
|
return f'<figure {fig_style}><img src="{image.image.url}" alt="{image.alt}" {style}><figcaption>{image.description}</figcaption></figure>'
|
||||||
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
from django_project.settings import STATIC_URL
|
||||||
|
|
||||||
TINYWIKI_USER_CONFIG = getattr(settings,
|
TINYWIKI_USER_CONFIG = getattr(settings,
|
||||||
"TINYWIKI_USER_CONFIG",
|
"TINYWIKI_USER_CONFIG",
|
||||||
{
|
{
|
||||||
@@ -12,8 +14,21 @@ TINYWIKI_USER_LOOKUP = getattr(settings,
|
|||||||
"TINYWIKI_USER_LOOKUP",
|
"TINYWIKI_USER_LOOKUP",
|
||||||
{'username':"TinyWiki"})
|
{'username':"TinyWiki"})
|
||||||
|
|
||||||
|
TINYWIKI_BRAND_LOGO = getattr(settings,
|
||||||
|
"TINYWIKI_BRAND_LOGO",
|
||||||
|
None)
|
||||||
|
TINYWIKI_BRAND_NAME = getattr(settings,
|
||||||
|
"TINYWIKI_BRAND_NAME",
|
||||||
|
"TinyWiki")
|
||||||
|
|
||||||
TINYWIKI_BOOSTRAP_TAGS = {
|
TINYWIKI_BOOSTRAP_TAGS = {
|
||||||
'img': {
|
'img': {
|
||||||
'class':'img-fluid',
|
'class':'img-fluid',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TINYWIKI_BASE_TEMPLATE = getattr(settings,
|
||||||
|
"TINYWIKI_BASE_TEMPLATE",
|
||||||
|
"tinywiki/base.html")
|
||||||
|
|
||||||
|
USE_BOOTSTRAP = getattr(settings,"USE_BOOTSTRAP",False)
|
||||||
|
|||||||
3
tinywiki/static/tinywiki/icons/book.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 772 B |
1
tinywiki/static/tinywiki/icons/bootstrap-icons.svg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
{% load i18n %}
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>TinyWiki</title>
|
||||||
|
{% block extra_css %}{% endblock %}
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<span style="font-size:2.5rem;">TinyWiki</span>
|
||||||
|
</header>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a href="{% url 'tinywiki:home' %}">{% translate "HOME" %}</a></li>
|
||||||
|
{% if user_can_create_wiki_pages %}
|
||||||
|
<li><a href="{% url 'tinywiki:page-create' %}">{% translate "Create Page" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><a href="{% url 'tinywiki:toc' %}">{% translate "Wiki content" %}</a>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<main class="content">
|
||||||
|
{% block content %}
|
||||||
|
<h1>It Works!</h1>
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
{% block extra_scripts %}{% endblock extra_scripts %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
44
tinywiki/templates/tinywiki/home/bs-wiki-content.html
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{% extends base_template %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% translate "Table of Contents" %}</h1>
|
||||||
|
|
||||||
|
{% for toc_section,pages_1,pages_2 in toc %}
|
||||||
|
<h2>{{ toc_section }}</h2>
|
||||||
|
<div class="row gx-4 mb-2">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<ul style="list-style-type:none;" class="mb-0 ps-0">
|
||||||
|
{% for page in pages_1 %}
|
||||||
|
<li>
|
||||||
|
<a class="icon-link icon-link-hover" href="{% url 'tinywiki:page' page.slug %}">
|
||||||
|
{{ page.title }}
|
||||||
|
{% if page.is_system %}
|
||||||
|
<svg class="bi"><use xlink:href="{% static 'icons/bootstrap-icons.svg' %}#journal" ></use></svg>
|
||||||
|
{% else %}
|
||||||
|
<svg class="bi"><use xlink:href="{% static 'icons/bootstrap-icons.svg' %}#book" ></use></svg>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<ul style="list-style-type:none;" class="mt-0 ps-0">
|
||||||
|
{% for page in pages_2 %}
|
||||||
|
<li>
|
||||||
|
<a class="icon-link icon-link-hover" href="{% url 'tinywiki:page' page.slug %}">
|
||||||
|
{{ page.title }}
|
||||||
|
{% if page.is_system %}
|
||||||
|
<svg class="bi"><use xlink:href="{% static 'icons/bootstrap-icons.svg' %}#journal" ></use></svg>
|
||||||
|
{% else %}
|
||||||
|
<svg class="bi"><use xlink:href="{% static 'icons/bootstrap-icons.svg' %}#book" ></use></svg>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor%}
|
||||||
|
{% endblock %}
|
||||||
32
tinywiki/templates/tinywiki/home/home.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{% extends base_template %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% 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 content %}
|
||||||
|
{% if page %}
|
||||||
|
<h1>{{ page.title }}</h1>
|
||||||
|
{{ page.html_content }}
|
||||||
|
{% 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>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
<script>hljs.highlightAll();</script>
|
||||||
|
{% endblock %}
|
||||||
18
tinywiki/templates/tinywiki/home/wiki-content.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{% extends base_template %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% translate "Table of contents" %}</h1>
|
||||||
|
|
||||||
|
{% for toc_section,pages_1,pages_2 in toc %}
|
||||||
|
<h2>{{ toc_section }}</h2>
|
||||||
|
<ul>
|
||||||
|
{% for page in pages_1 %}
|
||||||
|
<li><a href="{% url 'tinywiki:page' page.slug %}">{{ page.title }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
{% for page in pages_2 %}
|
||||||
|
<li><a href="{% url 'tinywiki:page' page.slug %}">{{ page.title }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endfor%}
|
||||||
|
{% endblock %}
|
||||||
3
tinywiki/templates/tinywiki/icons/book.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 772 B |
4
tinywiki/templates/tinywiki/icons/box-arrow-up-right.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 552 B |
4
tinywiki/templates/tinywiki/icons/file-earmark-x.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 485 B |
3
tinywiki/templates/tinywiki/icons/house.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 415 B |
4
tinywiki/templates/tinywiki/icons/journal.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 498 B |
26
tinywiki/templates/tinywiki/page/bs-view.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{% extends base_template %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% 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>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
<script>hljs.highlightAll();</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="text-end">
|
||||||
|
{% if user_can_edit_wiki_page %}
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<h1>{{ page.title }}</h1>
|
||||||
|
{{ page.html_content }}
|
||||||
|
{% endblock content %}
|
||||||
16
tinywiki/templates/tinywiki/page/create.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{% extends "tinywiki/page/model-base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% 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 %}
|
||||||
0
tinywiki/templates/tinywiki/page/delete-view.html
Normal file
16
tinywiki/templates/tinywiki/page/edit.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{% extends "tinywiki/page/model-base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% 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 %}
|
||||||
0
tinywiki/templates/tinywiki/page/hx-delete.html
Normal file
107
tinywiki/templates/tinywiki/page/model-base.html
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
{% extends base_template %}
|
||||||
|
{% load i18n widget_tweaks %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% block title %}{% endblock title%}</h1>
|
||||||
|
<form method="POST"
|
||||||
|
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 %}
|
||||||
|
{% endwith %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% with slug_extra|default:"" as slug_class %}
|
||||||
|
{{ form.slug.label_tag }}{% render_field form.slug class=slug_class %}
|
||||||
|
{% 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 %}
|
||||||
|
{% endwith %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% with status_extra|default:"" as 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 %}
|
||||||
|
{% endwith %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% block form_buttons %}{% endblock form_buttons %}
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 extra_scripts %}
|
||||||
24
tinywiki/templates/tinywiki/page/view.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% extends base_template %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% 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>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
<script>hljs.highlightAll();</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if user_can_edit_wiki_page %}
|
||||||
|
<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>
|
||||||
|
{% endif %}
|
||||||
|
<h1>{{ page.title }}</h1>
|
||||||
|
{{ page.html_content }}
|
||||||
|
{% endblock content %}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
from .views import *
|
||||||
|
|
||||||
app_name = "tinywiki"
|
app_name = "tinywiki"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path("",HomeView.as_view(),name="home"),
|
||||||
|
path("toc/",TocView.as_view(),name="toc"),
|
||||||
|
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'),
|
||||||
]
|
]
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
2
tinywiki/views/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .home import *
|
||||||
|
from .page import *
|
||||||
43
tinywiki/views/base.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from .. import settings
|
||||||
|
from django.views import View as DjangoView
|
||||||
|
from django.views.generic import FormView as DjangoFormView
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
class Base:
|
||||||
|
base_template_name = settings.TINYWIKI_BASE_TEMPLATE
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_base_template_name(cls)->str:
|
||||||
|
return cls.base_template_name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_template_name(cls)->str:
|
||||||
|
return cls.template_name
|
||||||
|
|
||||||
|
def get_tinywiki_context_data(self):
|
||||||
|
create_pages = False
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
if self.request.user.is_staff or self.request.user.has_perm('tinywiki.tinywiki-create'):
|
||||||
|
create_pages = True
|
||||||
|
return {
|
||||||
|
'brand_logo': settings.TINYWIKI_BRAND_LOGO,
|
||||||
|
'brand_name': settings.TINYWIKI_BRAND_NAME,
|
||||||
|
'base_template': self.get_base_template_name(),
|
||||||
|
'use_bootstrap': settings.USE_BOOTSTRAP,
|
||||||
|
'user_can_create_wiki_pages':create_pages
|
||||||
|
}
|
||||||
|
|
||||||
|
class View(Base,DjangoView):
|
||||||
|
template_name = "tinywiki"
|
||||||
|
|
||||||
|
def get_context_data(self,**kwargs):
|
||||||
|
context = self.get_tinywiki_context_data()
|
||||||
|
context.update(kwargs)
|
||||||
|
return context
|
||||||
|
|
||||||
|
class FormView(Base,DjangoFormView):
|
||||||
|
def get_context_data(self, **kwargs) -> dict[str, Any]:
|
||||||
|
context = self.get_tinywiki_context_data()
|
||||||
|
context.update(kwargs)
|
||||||
|
return super().get_context_data(**context)
|
||||||
|
|
||||||
94
tinywiki/views/home.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from curses.ascii import isalpha
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from tinywiki import settings
|
||||||
|
from .base import View
|
||||||
|
from django.http import HttpRequest,HttpResponse
|
||||||
|
from django.db.models.functions import Lower
|
||||||
|
import string
|
||||||
|
from ..models import Page
|
||||||
|
from ..enums import WikiPageStatus
|
||||||
|
from django.utils.translation import ngettext, gettext as _
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
|
|
||||||
|
class HomeView(View):
|
||||||
|
template_name = "tinywiki/home/home.html"
|
||||||
|
|
||||||
|
def get(self,request):
|
||||||
|
try:
|
||||||
|
page = Page.objects.get(slug='tw-home')
|
||||||
|
except Page.DoesNotExist:
|
||||||
|
page = None
|
||||||
|
|
||||||
|
return render(request,
|
||||||
|
self.get_template_name(),
|
||||||
|
self.get_context_data(page=page))
|
||||||
|
|
||||||
|
class TocView(View):
|
||||||
|
template_name = "tinywiki/home/wiki-content.html"
|
||||||
|
bs_template_name = "tinywiki/home/bs-wiki-content.html"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_template_name(cls):
|
||||||
|
if settings.USE_BOOTSTRAP:
|
||||||
|
return cls.bs_template_name
|
||||||
|
return cls.template_name
|
||||||
|
|
||||||
|
def get(self,request):
|
||||||
|
def mkdict(page:Page):
|
||||||
|
return {'slug':page.slug,'title':page.title, 'is_system':page.slug.startswith('tw-')}
|
||||||
|
|
||||||
|
user = self.request.user
|
||||||
|
if (user.is_staff or user.has_perm('page.read_all')):
|
||||||
|
pages = Page.objects.all()
|
||||||
|
else:
|
||||||
|
pages = Page.objects.filter(status_data=WikiPageStatus.PUBLISHED.value)
|
||||||
|
|
||||||
|
toc_mapping = {}
|
||||||
|
|
||||||
|
|
||||||
|
for page in pages:
|
||||||
|
first_char = page.title.lstrip()[0].upper()
|
||||||
|
if first_char.isdigit():
|
||||||
|
if '0-9' not in toc_mapping:
|
||||||
|
toc_mapping['0-9'] = [mkdict(page)]
|
||||||
|
else:
|
||||||
|
toc_mapping['0-9'].append(mkdict(page))
|
||||||
|
elif first_char in string.punctuation:
|
||||||
|
if '#' not in toc_mapping:
|
||||||
|
toc_mapping['#'] = [mkdict(page)]
|
||||||
|
else:
|
||||||
|
toc_mapping['#'].append(mkdict(page))
|
||||||
|
else:
|
||||||
|
if first_char not in toc_mapping:
|
||||||
|
toc_mapping[first_char] = [mkdict(page)]
|
||||||
|
else:
|
||||||
|
toc_mapping[first_char].append(mkdict(page))
|
||||||
|
|
||||||
|
toc = []
|
||||||
|
for key in sorted(toc_mapping.keys()):
|
||||||
|
toc_entries = toc_mapping[key]
|
||||||
|
count = len(toc_mapping[key])
|
||||||
|
split = (count + 1) // 2
|
||||||
|
pages_0 = toc_entries[:split]
|
||||||
|
if split >= count:
|
||||||
|
pages_1 = []
|
||||||
|
else:
|
||||||
|
pages_1 = toc_entries[split:]
|
||||||
|
|
||||||
|
|
||||||
|
if (key.startswith('0') or key == '#'):
|
||||||
|
toc_section = key
|
||||||
|
else:
|
||||||
|
toc_section = f"{key}..."
|
||||||
|
|
||||||
|
toc_section = ngettext("{toc_section} ({n} page)","{toc_section} ({n} pages)", count).format(
|
||||||
|
toc_section=toc_section,
|
||||||
|
n=count,
|
||||||
|
)
|
||||||
|
toc.append((toc_section,pages_0,pages_1))
|
||||||
|
|
||||||
|
return render(request,
|
||||||
|
self.get_template_name(),
|
||||||
|
self.get_context_data(toc=toc,subtitle=_("Table of Contents")))
|
||||||
261
tinywiki/views/page.py
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
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.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
|
||||||
|
|
||||||
|
class PageView(View):
|
||||||
|
template_name = "tinywiki/page/view.html"
|
||||||
|
bs_template_name = "tinywiki/page/bs-view.html"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_template_name(cls) -> str:
|
||||||
|
if settings.USE_BOOTSTRAP:
|
||||||
|
return cls.bs_template_name
|
||||||
|
return cls.template_name
|
||||||
|
|
||||||
|
def get_context_data(self,page,**kwargs):
|
||||||
|
can_edit_page = False
|
||||||
|
can_delete_page = False
|
||||||
|
user = self.request.user
|
||||||
|
if (user.is_staff):
|
||||||
|
can_edit_page = True
|
||||||
|
can_delete_page = True
|
||||||
|
elif page.slug.startswith('tw-'):
|
||||||
|
if user.has_perm('page.tinywiki-edit-system'):
|
||||||
|
can_edit_page = True
|
||||||
|
if user.has_perm('page.tinywiki-delete-system'):
|
||||||
|
can_delete_page = True
|
||||||
|
else:
|
||||||
|
if (user.has_perm('page.tinywiki-edit-all')
|
||||||
|
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'))):
|
||||||
|
can_delete_page = True
|
||||||
|
|
||||||
|
kwargs.update({'page':page,
|
||||||
|
'user_can_edit_wiki_page':can_edit_page,
|
||||||
|
'user_can_delete_wiki_page':can_delete_page,
|
||||||
|
'subtitle':page.title})
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get(self,request:HttpRequest,slug:str)->HttpResponse:
|
||||||
|
page = get_object_or_404(Page,slug=slug)
|
||||||
|
return render(request,
|
||||||
|
self.get_template_name(),
|
||||||
|
self.get_context_data(page=page))
|
||||||
|
|
||||||
|
class PageCreateView(LoginRequiredMixin,UserPassesTestMixin,FormView):
|
||||||
|
template_name = "tinywiki/page/create.html"
|
||||||
|
form_class = PageForm
|
||||||
|
|
||||||
|
def test_func(self) -> bool:
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
if self.request.user.is_staff:
|
||||||
|
return True
|
||||||
|
return self.request.user.has_perm('tinywiki.tinywiki-create')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_context_data(self,**kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['create']=True
|
||||||
|
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(
|
||||||
|
slug_extra=slug_extra,
|
||||||
|
title_extra=title_extra,
|
||||||
|
status_extra=status_extra,
|
||||||
|
content_type_extra=content_type_extra,
|
||||||
|
content_extra=content_extra,
|
||||||
|
))
|
||||||
|
|
||||||
|
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')):
|
||||||
|
return render(self.request,
|
||||||
|
self.get_template_name(),
|
||||||
|
self.get_context_data(
|
||||||
|
slug_extra="is-invalid",
|
||||||
|
title_extra="is-valid",
|
||||||
|
status_extra="is-valid",
|
||||||
|
content_type_extra="is-valid",
|
||||||
|
content_extra="is-valid",
|
||||||
|
))
|
||||||
|
|
||||||
|
instance.author = user
|
||||||
|
instance.created_by = user
|
||||||
|
instance.last_edited_by = user
|
||||||
|
try:
|
||||||
|
form.save(commit=True)
|
||||||
|
if self.request.GET.get('save',"0") == "1":
|
||||||
|
return redirect(reverse("tinywiki:page-edit",kwargs={"slug":instance.slug}))
|
||||||
|
return redirect(reverse("tinywiki:page",kwargs={'slug':instance.slug}))
|
||||||
|
except:
|
||||||
|
return render(self.request,
|
||||||
|
self.get_template_name(),
|
||||||
|
self.get_context_data(
|
||||||
|
slug_extra="is-invalid",
|
||||||
|
title_extra="is-valid",
|
||||||
|
status_extra="is-valid",
|
||||||
|
content_type_extra="is-valid",
|
||||||
|
content_extra="is-valid",
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
class PageEditView(LoginRequiredMixin,UserPassesTestMixin,FormView):
|
||||||
|
template_name = "tinywiki/page/edit.html"
|
||||||
|
form_class = PageForm
|
||||||
|
|
||||||
|
def test_func(self) -> bool:
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
if self.request.user.is_staff:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
def get_context_data(self,**kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['create']=False
|
||||||
|
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(
|
||||||
|
slug_extra=slug_extra,
|
||||||
|
title_extra=title_extra,
|
||||||
|
status_extra=status_extra,
|
||||||
|
content_type_extra=content_type_extra,
|
||||||
|
content_extra=content_extra,
|
||||||
|
))
|
||||||
|
|
||||||
|
def get(self,request,slug:str):
|
||||||
|
instance = get_object_or_404(Page,slug=slug)
|
||||||
|
user = request.user
|
||||||
|
if (instance.slug.startswith('tw-')
|
||||||
|
and not user.is_staff
|
||||||
|
and not user.has_perm('page.tinywiki-edit-system')):
|
||||||
|
raise PermissionDenied(_("Only staff users and wiki-admins 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().get(request)
|
||||||
|
|
||||||
|
def post(self,request,slug:str):
|
||||||
|
instance = get_object_or_404(Page,slug=slug)
|
||||||
|
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)
|
||||||
|
if self.request.GET.get('save',"0") == "1":
|
||||||
|
return redirect(reverse("tinywiki:page-edit",kwargs={"slug":instance.slug}))
|
||||||
|
return redirect(reverse("tinywiki:page",kwargs={'slug':instance.slug}))
|
||||||
|
except:
|
||||||
|
return render(self.request,
|
||||||
|
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()
|
||||||
@@ -10,7 +10,7 @@ class Migration(migrations.Migration):
|
|||||||
from user.models import UserProfile
|
from user.models import UserProfile
|
||||||
from allauth.account.models import EmailAddress
|
from allauth.account.models import EmailAddress
|
||||||
|
|
||||||
if settings.DEBUG and UserProfile.objects.count() == 0:
|
if settings.DEBUG and UserProfile.objects.filter(is_superuser=True).count() == 0:
|
||||||
user = UserProfile.objects.create_superuser("debug-admin","debug-admin@example.com","Pa55w.rd")
|
user = UserProfile.objects.create_superuser("debug-admin","debug-admin@example.com","Pa55w.rd")
|
||||||
account_emailaddress = EmailAddress.objects.create(user=user,email=user.email,verified=True,primary=True)
|
account_emailaddress = EmailAddress.objects.create(user=user,email=user.email,verified=True,primary=True)
|
||||||
|
|
||||||
|
|||||||