diff --git a/Dockerfile.devel.yml b/Dockerfile.devel.yml
index f090bce..ee08242 100644
--- a/Dockerfile.devel.yml
+++ b/Dockerfile.devel.yml
@@ -21,6 +21,6 @@ COPY . .
RUN poetry install --all-groups \
&& chmod 0755 start-django.sh
-
+HEALTHCHECK --interval=30s --retries=5 --timeout=30s CMD curl -sS 127.0.0.1:8000
EXPOSE 8000
ENTRYPOINT ["/app/start-django.sh"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..60f6968
--- /dev/null
+++ b/LICENSE
@@ -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.
\ No newline at end of file
diff --git a/django_project/settings.py b/django_project/settings.py
index cf3f067..4961555 100644
--- a/django_project/settings.py
+++ b/django_project/settings.py
@@ -11,13 +11,20 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
+from django.urls import reverse_lazy
+
import sys
from environ import Env
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
+LOCAL_DIR = Path(__file__).resolve().parent / "local"
+
ENV = Env(
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("\\","/")}"),
ALLOWED_HOSTS=(list,['*']),
STATIC_URL=(str,"static/"),
@@ -28,15 +35,18 @@ ENV = Env(
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")
+
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():
ENV.read_env(_env_file)
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():
ENV.read_env(_env_file)
del _env_file
@@ -119,7 +129,7 @@ ROOT_URLCONF = 'django_project.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [],
+ 'DIRS': [BASE_DIR/"templates"],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@@ -174,6 +184,8 @@ USE_I18N = True
USE_TZ = True
+# Auth settings
+LOGIN_REDIRECT_URL = reverse_lazy('tinywiki:home')
# Static files (CSS, JavaScript, Images)
# 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
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+STATICFILES_DIRS = [
+ BASE_DIR/"static",
+]
if ENV("EMAIL_BACKEND") == 'smtp':
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
@@ -200,5 +215,25 @@ else:
if ENV("EMAIL_BACKEND") != 'console':
print("Email backend not known falling back to console!",file=sys.stderr)
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
diff --git a/django_project/urls.py b/django_project/urls.py
index 6d2693e..09af0ed 100644
--- a/django_project/urls.py
+++ b/django_project/urls.py
@@ -21,6 +21,7 @@ urlpatterns = [
path('',include("tinywiki.urls")),
path("user/",include("user.urls")),
path('admin/', admin.site.urls),
+ path('accounts/',include('allauth.urls')),
]
if settings.DEBUG:
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..a1c54f3
--- /dev/null
+++ b/package-lock.json
@@ -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"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ad0d0ab
--- /dev/null
+++ b/package.json
@@ -0,0 +1,6 @@
+{
+ "dependencies": {
+ "bootstrap": "^5.3.8",
+ "bootstrap-icons": "^1.13.1"
+ }
+}
diff --git a/static/icons/bootstrap-icons.svg b/static/icons/bootstrap-icons.svg
new file mode 100644
index 0000000..fa7e620
--- /dev/null
+++ b/static/icons/bootstrap-icons.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..30159ee
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,90 @@
+
+{% load i18n static %}
+
+
+
+
+ TinyWiki
+
+ {% block extra_css %}{% endblock %}
+
+ {% block scripts %}{% endblock %}
+
+
+
+
+
+
+ {% if brand_logo %}
+
+ {% else %}
+
+
+
+ {% endif %}
+
{{ brand_name }}
+ {% if subtitle %}
+
{{ subtitle }}
+ {% endif %}
+
+
+
+
+
+
+
+
+ {% block content %}
+ It Works!
+ {% endblock %}
+
+
+
+
+
+
+
+
+ Powered by TinyWiki
+
+
+ © 2025
+
+
+
+
+ {% block extra_scripts %}{% endblock extra_scripts %}
+
+
\ No newline at end of file
diff --git a/tinywiki/enums.py b/tinywiki/enums.py
index b76539d..7c55861 100644
--- a/tinywiki/enums.py
+++ b/tinywiki/enums.py
@@ -34,9 +34,53 @@ class WikiContentType(StrEnum):
def __repr__(self):
return f"<{self.__qualname__}: {self.value.upper()}>"
-
WIKI_CONTENT_TYPES = (
WikiContentType.MARKDOWN,
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,
+)
\ No newline at end of file
diff --git a/tinywiki/forms.py b/tinywiki/forms.py
new file mode 100644
index 0000000..bc0de6a
--- /dev/null
+++ b/tinywiki/forms.py
@@ -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',
+ ]
+
\ No newline at end of file
diff --git a/tinywiki/migrations/0001_initial.py b/tinywiki/migrations/0001_initial.py
index f3f46bb..a13484c 100644
--- a/tinywiki/migrations/0001_initial.py
+++ b/tinywiki/migrations/0001_initial.py
@@ -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 tinywiki.models
from django.conf import settings
from django.db import migrations, models
@@ -22,8 +23,8 @@ class Migration(migrations.Migration):
('alt', models.CharField(max_length=511, verbose_name='alternative text')),
('description', models.CharField(blank=True, max_length=1023, null=True, verbose_name='description')),
('image', models.ImageField(upload_to='tinywiki/img', verbose_name='image file')),
- ('uploaded_at', models.DateTimeField(auto_now_add=True, verbose_name='upladed 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_at', models.DateTimeField(auto_now_add=True, verbose_name='uploaded at')),
+ ('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(
@@ -32,13 +33,14 @@ class Migration(migrations.Migration):
('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')),
('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', models.TextField(verbose_name='Page content')),
('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')),
- ('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')),
- ('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')),
- ('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')),
+ ('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(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(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')),
],
),
]
diff --git a/tinywiki/migrations/0002_initial_data.py b/tinywiki/migrations/0002_initial_data.py
index 1956752..e286abe 100644
--- a/tinywiki/migrations/0002_initial_data.py
+++ b/tinywiki/migrations/0002_initial_data.py
@@ -9,18 +9,90 @@ class Migration(migrations.Migration):
def init_tinywiki_user(apps,schema_editor):
from django.contrib.auth import get_user_model
- user = get_user_model.objects.create_user(**settings.TINYWIKI_USER_CONFIG)
+ 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:
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:
from ..models import Page,Image
#TODO
operations = [
+ migrations.RunPython(init_tinywiki_groups_and_permissions),
migrations.RunPython(init_tinywiki_user),
- migrations.RunPython(init_default_pages)
+ migrations.RunPython(init_default_pages),
]
\ No newline at end of file
diff --git a/tinywiki/migrations/pages/bs-license.bbcode b/tinywiki/migrations/pages/bs-license.bbcode
new file mode 100644
index 0000000..1dc0dcd
--- /dev/null
+++ b/tinywiki/migrations/pages/bs-license.bbcode
@@ -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]
\ No newline at end of file
diff --git a/tinywiki/migrations/pages/license.bbcode b/tinywiki/migrations/pages/license.bbcode
new file mode 100644
index 0000000..031840f
--- /dev/null
+++ b/tinywiki/migrations/pages/license.bbcode
@@ -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]
\ No newline at end of file
diff --git a/tinywiki/migrations/pages/pages.json b/tinywiki/migrations/pages/pages.json
new file mode 100644
index 0000000..88d330a
--- /dev/null
+++ b/tinywiki/migrations/pages/pages.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/tinywiki/models.py b/tinywiki/models.py
index e61ceed..4c789b4 100644
--- a/tinywiki/models.py
+++ b/tinywiki/models.py
@@ -4,10 +4,21 @@ from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.contrib.auth import get_user_model
from django.utils.safestring import mark_safe,SafeText
+from django.contrib.auth import get_user_model
-from tinywiki.enums import WIKI_CONTENT_TYPES, WikiContentType
+from tinywiki.enums import WIKI_CONTENT_TYPES, WIKI_PAGE_STATUS, WikiContentType, WikiPageStatus
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):
slug = models.SlugField(_("slug"),
@@ -20,11 +31,16 @@ class Page(models.Model):
null=False,
blank=False)
author = models.ForeignKey(get_user_model(),
- on_delete=models.SET_NULL,
+ on_delete=models.SET_DEFAULT,
+ default=get_tinywiki_default_user,
verbose_name=_("author"),
- null=True,
- blank=True,
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"),
choices=[(i.value,i.str_lazy) for i in WIKI_CONTENT_TYPES],
default=WikiContentType.BBCODE.value)
@@ -37,34 +53,52 @@ class Page(models.Model):
created_at = models.DateTimeField(_("created at"),
auto_now_add=True)
created_by = models.ForeignKey(get_user_model(),
- on_delete=models.SET_NULL,
+ on_delete=models.SET_DEFAULT,
verbose_name=_("created by"),
- null=True,
- blank=True,
+ default=get_tinywiki_default_user,
related_name="tinywiki_created")
last_edited_at = models.DateTimeField(_("last edited at"),
auto_now=True)
last_edited_by = models.ForeignKey(get_user_model(),
- on_delete=models.SET_NULL,
+ on_delete=models.SET_DEFAULT,
verbose_name=_("last edited by"),
- null=True,
- blank=True,
+ default=get_tinywiki_default_user,
related_name="tinywiki_last_edited")
@property
def content_type(self)->WikiContentType:
return WikiContentType.from_string(self.content_type_data)
-
+ @content_type.setter
+ def content_type(self,content_type:str|WikiContentType):
+ if isinstance(content_type,str):
+ 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
def html_content(self)->SafeText|str:
if self.content_type == WikiContentType.MARKDOWN:
return mark_safe(markdown.markdown(self.content))
elif self.content_type == WikiContentType.BBCODE:
- return mark_safe(bbcode.render_html(self.content))
+ return mark_safe(parse_bbcode(self.content))
return self.content
-
+
class Image(models.Model):
models.ManyToManyField(Page, verbose_name=_(""))
slug = models.SlugField(_("slug"),
@@ -81,10 +115,9 @@ class Image(models.Model):
upload_to="tinywiki/img")
uploaded_by = models.ForeignKey(get_user_model(),
- on_delete=models.SET_NULL,
+ on_delete=models.SET_DEFAULT,
verbose_name=_("uploaded by"),
- null=True,
- blank=True,
+ default=get_tinywiki_default_user,
related_name="tinywiki_image_uploads")
uploaded_at = models.DateTimeField(_("uploaded at"),
auto_now_add=True)
diff --git a/tinywiki/parser/__init__.py b/tinywiki/parser/__init__.py
new file mode 100644
index 0000000..bea5413
--- /dev/null
+++ b/tinywiki/parser/__init__.py
@@ -0,0 +1,4 @@
+from .bbcode import PARSER as BBCODE_PARSER
+
+def parse_bbcode(text:str):
+ return BBCODE_PARSER.format(text)
diff --git a/tinywiki/parser/bbcode/__init__.py b/tinywiki/parser/bbcode/__init__.py
new file mode 100644
index 0000000..0f9b83d
--- /dev/null
+++ b/tinywiki/parser/bbcode/__init__.py
@@ -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 _
\ No newline at end of file
diff --git a/tinywiki/parser/bbcode/formatters.py b/tinywiki/parser/bbcode/formatters.py
new file mode 100644
index 0000000..6392326
--- /dev/null
+++ b/tinywiki/parser/bbcode/formatters.py
@@ -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})
+]
diff --git a/tinywiki/parser/bbcode/simple_formatters.py b/tinywiki/parser/bbcode/simple_formatters.py
new file mode 100644
index 0000000..5783c28
--- /dev/null
+++ b/tinywiki/parser/bbcode/simple_formatters.py
@@ -0,0 +1,12 @@
+
+SIMPLE_HEADER_FORMATTERS = [
+ (('h1',"%(value)s "),{}),
+ (('h2',"%(value)s "),{}),
+ (('h3',"%(value)s "),{}),
+ (('h4',"%(value)s "),{}),
+ (('h5',"%(value)s "),{}),
+ (('h6',"%(value)s "),{}),
+ (('copy',"©"),{'standalone':True}),
+ (('reg',"®"),{'standalone':True}),
+ (('trade',"™"),{'standalone':True}),
+]
\ No newline at end of file
diff --git a/tinywiki/parser/bbcode/text_formatters.py b/tinywiki/parser/bbcode/text_formatters.py
new file mode 100644
index 0000000..beec7b5
--- /dev/null
+++ b/tinywiki/parser/bbcode/text_formatters.py
@@ -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"{value}{render_to_string('tinywiki/icons/box-arrow-up-right.svg')} "
+ return f"{value} "
+
+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"{value}{svg} "
+ return f"{value} "
+
+
+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"{value}{render_to_string(svg_template)} "
+ return f"{value} "
+
+def render_codeblock(tag_name:str,value,options,parent,context)->str:
+ if 'codeblock' in options:
+ return f"{value} "
+ return f"{value} "
+
+def render_ordered_list(tag_name:str,value,options,parent,context)->str:
+ return f"{value} "
+
+def render_unordered_list(tag_name:str,value,options,parent,context)->str:
+ return f""
+
+def render_list_item(tag_name:str,value,options,parent,context)->str:
+ return f"{value} "
+
+
+def render_paragraph(tag_name:str,value,options,parent,context):
+ if settings.USE_BOOTSTRAP:
+ return f"{value}
"
+ return f"{value}
"
+
+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'{value} '
+ else:
+ return f'{value} '
+
+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'{image.description} '
+ else:
+ return f'{image.description} '
+
diff --git a/tinywiki/settings.py b/tinywiki/settings.py
index 22cdbd3..f63a0f1 100644
--- a/tinywiki/settings.py
+++ b/tinywiki/settings.py
@@ -1,6 +1,8 @@
from pathlib import Path
from django.conf import settings
+from django_project.settings import STATIC_URL
+
TINYWIKI_USER_CONFIG = getattr(settings,
"TINYWIKI_USER_CONFIG",
{
@@ -12,8 +14,21 @@ TINYWIKI_USER_LOOKUP = getattr(settings,
"TINYWIKI_USER_LOOKUP",
{'username':"TinyWiki"})
+TINYWIKI_BRAND_LOGO = getattr(settings,
+ "TINYWIKI_BRAND_LOGO",
+ None)
+TINYWIKI_BRAND_NAME = getattr(settings,
+ "TINYWIKI_BRAND_NAME",
+ "TinyWiki")
+
TINYWIKI_BOOSTRAP_TAGS = {
'img': {
'class':'img-fluid',
}
}
+
+TINYWIKI_BASE_TEMPLATE = getattr(settings,
+ "TINYWIKI_BASE_TEMPLATE",
+ "tinywiki/base.html")
+
+USE_BOOTSTRAP = getattr(settings,"USE_BOOTSTRAP",False)
diff --git a/tinywiki/static/tinywiki/icons/book.svg b/tinywiki/static/tinywiki/icons/book.svg
new file mode 100644
index 0000000..d5ac198
--- /dev/null
+++ b/tinywiki/static/tinywiki/icons/book.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/tinywiki/static/tinywiki/icons/bootstrap-icons.svg b/tinywiki/static/tinywiki/icons/bootstrap-icons.svg
new file mode 100644
index 0000000..fa7e620
--- /dev/null
+++ b/tinywiki/static/tinywiki/icons/bootstrap-icons.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tinywiki/templates/tinywiki/base.html b/tinywiki/templates/tinywiki/base.html
index e69de29..c839dbc 100644
--- a/tinywiki/templates/tinywiki/base.html
+++ b/tinywiki/templates/tinywiki/base.html
@@ -0,0 +1,31 @@
+
+{% load i18n %}
+
+
+
+
+ TinyWiki
+ {% block extra_css %}{% endblock %}
+ {% block scripts %}{% endblock %}
+
+
+
+
+
+
+
+ {% block content %}
+ It Works!
+ {% endblock %}
+
+ {% block extra_scripts %}{% endblock extra_scripts %}
+
+
\ No newline at end of file
diff --git a/tinywiki/templates/tinywiki/home/bs-wiki-content.html b/tinywiki/templates/tinywiki/home/bs-wiki-content.html
new file mode 100644
index 0000000..11375a0
--- /dev/null
+++ b/tinywiki/templates/tinywiki/home/bs-wiki-content.html
@@ -0,0 +1,44 @@
+{% extends base_template %}
+{% load i18n static %}
+
+{% block content %}
+{% translate "Table of Contents" %}
+
+{% for toc_section,pages_1,pages_2 in toc %}
+{{ toc_section }}
+
+{% endfor%}
+{% endblock %}
diff --git a/tinywiki/templates/tinywiki/home/home.html b/tinywiki/templates/tinywiki/home/home.html
new file mode 100644
index 0000000..619ce6b
--- /dev/null
+++ b/tinywiki/templates/tinywiki/home/home.html
@@ -0,0 +1,32 @@
+{% extends base_template %}
+{% load i18n %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+{% if page %}
+{{ page.title }}
+{{ page.html_content }}
+{% else %}
+Welcome to TinyWiki
+
+{% 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 tw-home and put the content for your welcome-page there.{% endblocktranslate %}
+{% blocktranslate %}You can use Markdown or BBCode to write your pages. If you don't know
+ Markdown read the Guide for Markdown used by TinyWiki . Or if you want to use
+ BBCode there is a Guide for BBCode used by TinyWiki too.{% endblocktranslate %}
+
+{% endif %}
+{% endblock content %}
+
+
+{% block scripts %}
+
+{% endblock %}
+
+{% block extra_scripts %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/tinywiki/templates/tinywiki/home/wiki-content.html b/tinywiki/templates/tinywiki/home/wiki-content.html
new file mode 100644
index 0000000..25cf2cb
--- /dev/null
+++ b/tinywiki/templates/tinywiki/home/wiki-content.html
@@ -0,0 +1,18 @@
+{% extends base_template %}
+{% load i18n %}
+
+{% block content %}
+{% translate "Table of contents" %}
+
+{% for toc_section,pages_1,pages_2 in toc %}
+{{ toc_section }}
+
+{% endfor%}
+{% endblock %}
diff --git a/tinywiki/templates/tinywiki/icons/book.svg b/tinywiki/templates/tinywiki/icons/book.svg
new file mode 100644
index 0000000..d5ac198
--- /dev/null
+++ b/tinywiki/templates/tinywiki/icons/book.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/tinywiki/templates/tinywiki/icons/box-arrow-up-right.svg b/tinywiki/templates/tinywiki/icons/box-arrow-up-right.svg
new file mode 100644
index 0000000..a546ece
--- /dev/null
+++ b/tinywiki/templates/tinywiki/icons/box-arrow-up-right.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/tinywiki/templates/tinywiki/icons/file-earmark-x.svg b/tinywiki/templates/tinywiki/icons/file-earmark-x.svg
new file mode 100644
index 0000000..be8e967
--- /dev/null
+++ b/tinywiki/templates/tinywiki/icons/file-earmark-x.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/tinywiki/templates/tinywiki/icons/house.svg b/tinywiki/templates/tinywiki/icons/house.svg
new file mode 100644
index 0000000..cb57f68
--- /dev/null
+++ b/tinywiki/templates/tinywiki/icons/house.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/tinywiki/templates/tinywiki/icons/journal.svg b/tinywiki/templates/tinywiki/icons/journal.svg
new file mode 100644
index 0000000..ceb4ca1
--- /dev/null
+++ b/tinywiki/templates/tinywiki/icons/journal.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/tinywiki/templates/tinywiki/page/bs-view.html b/tinywiki/templates/tinywiki/page/bs-view.html
new file mode 100644
index 0000000..f6a67f5
--- /dev/null
+++ b/tinywiki/templates/tinywiki/page/bs-view.html
@@ -0,0 +1,26 @@
+{% extends base_template %}
+{% load i18n %}
+
+{% block extra_css %}
+
+{% endblock %}
+{% block scripts %}
+
+{% endblock %}
+
+{% block extra_scripts %}
+
+{% endblock %}
+
+{% block content %}
+
+ {% if user_can_edit_wiki_page %}
+
{% translate "Edit Page" %}
+ {% endif %}
+ {% if user_can_delete_wiki_page %}
+
Delete Page
+ {% endif %}
+
+{{ page.title }}
+{{ page.html_content }}
+{% endblock content %}
\ No newline at end of file
diff --git a/tinywiki/templates/tinywiki/page/create.html b/tinywiki/templates/tinywiki/page/create.html
new file mode 100644
index 0000000..3086da1
--- /dev/null
+++ b/tinywiki/templates/tinywiki/page/create.html
@@ -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 %}
+
+
{% translate "Create Page" %}
+
{% translate "Cancel" %}
+
+{% else %}
+{% translate "Create Page" %}
+{% translate "Cancel" %}
+{% endif %}
+{% endblock form_buttons %}
\ No newline at end of file
diff --git a/tinywiki/templates/tinywiki/page/delete-view.html b/tinywiki/templates/tinywiki/page/delete-view.html
new file mode 100644
index 0000000..e69de29
diff --git a/tinywiki/templates/tinywiki/page/edit.html b/tinywiki/templates/tinywiki/page/edit.html
new file mode 100644
index 0000000..ecbd0c0
--- /dev/null
+++ b/tinywiki/templates/tinywiki/page/edit.html
@@ -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 %}
+
+
{% translate "Save Changes" %}
+
{% translate "Cancel" %}
+
+{% else %}
+{% translate "Save Changes" %}
+{% translate "Cancel" %}
+{% endif %}
+{% endblock form_buttons %}
\ No newline at end of file
diff --git a/tinywiki/templates/tinywiki/page/hx-delete.html b/tinywiki/templates/tinywiki/page/hx-delete.html
new file mode 100644
index 0000000..e69de29
diff --git a/tinywiki/templates/tinywiki/page/model-base.html b/tinywiki/templates/tinywiki/page/model-base.html
new file mode 100644
index 0000000..4afaa6f
--- /dev/null
+++ b/tinywiki/templates/tinywiki/page/model-base.html
@@ -0,0 +1,107 @@
+{% extends base_template %}
+{% load i18n widget_tweaks %}
+
+{% block content %}
+{% block title %}{% endblock title%}
+
+{% endblock content%}
+
+{% block extra_scripts %}
+
+{% endblock extra_scripts %}
\ No newline at end of file
diff --git a/tinywiki/templates/tinywiki/page/view.html b/tinywiki/templates/tinywiki/page/view.html
new file mode 100644
index 0000000..0f4a3ee
--- /dev/null
+++ b/tinywiki/templates/tinywiki/page/view.html
@@ -0,0 +1,24 @@
+{% extends base_template %}
+{% load i18n %}
+
+{% block extra_css %}
+
+{% endblock %}
+{% block scripts %}
+
+{% endblock %}
+
+{% block extra_scripts %}
+
+{% endblock %}
+
+{% block content %}
+{% if user_can_edit_wiki_page %}
+ {% translate "Edit Page" %}
+{% endif %}
+{% if user_can_delete_wiki_page %}
+Delete Page
+{% endif %}
+{{ page.title }}
+{{ page.html_content }}
+{% endblock content %}
\ No newline at end of file
diff --git a/tinywiki/urls.py b/tinywiki/urls.py
index 42dfa18..3bc16af 100644
--- a/tinywiki/urls.py
+++ b/tinywiki/urls.py
@@ -1,7 +1,13 @@
from django.urls import path
+from .views import *
+
app_name = "tinywiki"
urlpatterns = [
-
+ path("",HomeView.as_view(),name="home"),
+ path("toc/",TocView.as_view(),name="toc"),
+ path("page//",PageView.as_view(),name="page"),
+ path("page-create/",PageCreateView.as_view(),name="page-create"),
+ path("page//edit/",PageEditView.as_view(),name='page-edit'),
]
\ No newline at end of file
diff --git a/tinywiki/views.py b/tinywiki/views.py
deleted file mode 100644
index c60c790..0000000
--- a/tinywiki/views.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.shortcuts import render
-
-# Create your views here.
diff --git a/tinywiki/views/__init__.py b/tinywiki/views/__init__.py
new file mode 100644
index 0000000..bded29d
--- /dev/null
+++ b/tinywiki/views/__init__.py
@@ -0,0 +1,2 @@
+from .home import *
+from .page import *
\ No newline at end of file
diff --git a/tinywiki/views/base.py b/tinywiki/views/base.py
new file mode 100644
index 0000000..890ca17
--- /dev/null
+++ b/tinywiki/views/base.py
@@ -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)
+
\ No newline at end of file
diff --git a/tinywiki/views/home.py b/tinywiki/views/home.py
new file mode 100644
index 0000000..4f19657
--- /dev/null
+++ b/tinywiki/views/home.py
@@ -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")))
\ No newline at end of file
diff --git a/tinywiki/views/page.py b/tinywiki/views/page.py
new file mode 100644
index 0000000..b493de7
--- /dev/null
+++ b/tinywiki/views/page.py
@@ -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()
\ No newline at end of file
diff --git a/user/migrations/0002_default_superuser.py b/user/migrations/0002_default_superuser.py
index 95fbf23..bb39e4c 100644
--- a/user/migrations/0002_default_superuser.py
+++ b/user/migrations/0002_default_superuser.py
@@ -10,7 +10,7 @@ class Migration(migrations.Migration):
from user.models import UserProfile
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")
account_emailaddress = EmailAddress.objects.create(user=user,email=user.email,verified=True,primary=True)