From eec925eef140f2652489dda7bbbf1d1e401d0982 Mon Sep 17 00:00:00 2001 From: Dirk Alders Date: Sat, 5 Oct 2024 11:55:55 +0200 Subject: [PATCH] Initial piki commit with first rough functionality --- .gitignore | 5 + .gitmodules | 12 ++ .vscode/launch.json | 17 ++ .vscode/settings.json | 14 ++ README.md | 4 +- config.py | 12 ++ data/media/theme/logo.png | Bin 0 -> 6458 bytes fstools | 1 + manage.py | 22 +++ mycreole | 1 + pages/__init__.py | 9 + pages/access.py | 8 + pages/admin.py | 3 + pages/apps.py | 6 + pages/context.py | 289 ++++++++++++++++++++++++++++++++ pages/creole.py | 27 +++ pages/help.py | 58 +++++++ pages/migrations/__init__.py | 0 pages/models.py | 3 + pages/page.py | 47 ++++++ pages/templates/pages/page.html | 5 + pages/tests.py | 3 + pages/views.py | 56 +++++++ piki/__init__.py | 0 piki/asgi.py | 16 ++ piki/settings.py | 178 ++++++++++++++++++++ piki/urls.py | 38 +++++ piki/wsgi.py | 16 ++ requirements.txt | 5 + themes | 1 + users | 1 + 31 files changed, 856 insertions(+), 1 deletion(-) create mode 100644 .gitmodules create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 config.py create mode 100644 data/media/theme/logo.png create mode 160000 fstools create mode 100755 manage.py create mode 160000 mycreole create mode 100644 pages/__init__.py create mode 100644 pages/access.py create mode 100644 pages/admin.py create mode 100644 pages/apps.py create mode 100644 pages/context.py create mode 100644 pages/creole.py create mode 100644 pages/help.py create mode 100644 pages/migrations/__init__.py create mode 100644 pages/models.py create mode 100644 pages/page.py create mode 100644 pages/templates/pages/page.html create mode 100644 pages/tests.py create mode 100644 pages/views.py create mode 100644 piki/__init__.py create mode 100644 piki/asgi.py create mode 100644 piki/settings.py create mode 100644 piki/urls.py create mode 100644 piki/wsgi.py create mode 100644 requirements.txt create mode 160000 themes create mode 160000 users diff --git a/.gitignore b/.gitignore index d7e83b7..5ce3353 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +# piki +data/pages +data/static +db.sqlite3 + # ---> Python # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..02a82e8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "fstools"] + path = fstools + url = https://git.mount-mockery.de/pylib/fstools.git +[submodule "mycreole"] + path = mycreole + url = https://git.mount-mockery.de/django_lib/mycreole.git +[submodule "themes"] + path = themes + url = https://git.mount-mockery.de/django_lib/themes.git +[submodule "users"] + path = users + url = https://git.mount-mockery.de/django_lib/users.git diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a959ed3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Verwendet IntelliSense zum Ermitteln möglicher Attribute. + // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. + // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Main File execution", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/manage.py", + "args": ["runserver"], + "console": "integratedTerminal", + "justMyCode": true + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ee25f90 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "python.defaultInterpreterPath": "./venv/bin/python", + "autopep8.args": ["--max-line-length=150"], + "[python]": { + "python.formatting.provider": "none", + "editor.defaultFormatter": "ms-python.autopep8", + "editor.formatOnSave": true + }, + "editor.fontSize": 14, + "emmet.includeLanguages": { "django-html": "html" }, + "python.testing.pytestArgs": ["-v", "--cov", "--cov-report=xml", "__test__"], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} diff --git a/README.md b/README.md index b7ea764..ee43913 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # piki -Piki is a minimal wiki \ No newline at end of file +Piki is a minimal wiki + +# TODO: Add an installation instruction diff --git a/config.py b/config.py new file mode 100644 index 0000000..77ba7dc --- /dev/null +++ b/config.py @@ -0,0 +1,12 @@ +import os +# +# +APP_NAME = "piki" +STARTPAGE = "startpage" + + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True +# +SECRET_KEY = "*+=wy%2lo2^@fxxtmx0)14x507%6v73ke-%24%_fb6f+3h^c)-" +# diff --git a/data/media/theme/logo.png b/data/media/theme/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..508bfae22a13387ac0bf859d82a85641b1aced76 GIT binary patch literal 6458 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 Uv!8zfQUCw|07*qoM6N<$f>i@-umAu6 literal 0 HcmV?d00001 diff --git a/fstools b/fstools new file mode 160000 index 0000000..c10e879 --- /dev/null +++ b/fstools @@ -0,0 +1 @@ +Subproject commit c10e8792abb05671dab6de51cdadda3bf8ead50f diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..9317a79 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'piki.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/mycreole b/mycreole new file mode 160000 index 0000000..057388e --- /dev/null +++ b/mycreole @@ -0,0 +1 @@ +Subproject commit 057388e3b44b5fe42b133fe030c41aa6a254de58 diff --git a/pages/__init__.py b/pages/__init__.py new file mode 100644 index 0000000..8270097 --- /dev/null +++ b/pages/__init__.py @@ -0,0 +1,9 @@ +from django.urls.base import reverse + + +def url_page(request, rel_path): + return reverse('pages-pages', kwargs={'rel_path': rel_path}) + + +def url_helpview(request, page): + return reverse('pages-helpview', kwargs={'page': page}) diff --git a/pages/access.py b/pages/access.py new file mode 100644 index 0000000..401730a --- /dev/null +++ b/pages/access.py @@ -0,0 +1,8 @@ +# TODO: Implement access control for pages + +def read_attachment(request, rel_path): + return True + + +def modify_attachment(request, rel_path): + return True diff --git a/pages/admin.py b/pages/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/pages/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/pages/apps.py b/pages/apps.py new file mode 100644 index 0000000..cdd024b --- /dev/null +++ b/pages/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PagesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'pages' diff --git a/pages/context.py b/pages/context.py new file mode 100644 index 0000000..83dcf91 --- /dev/null +++ b/pages/context.py @@ -0,0 +1,289 @@ +import inspect +import logging + +from django.utils.translation import gettext as _ + + +from .help import actionbar as actionbar_add_help +import mycreole +import pages +from themes import empty_entry_parameters, gray_icon_url, color_icon_url +from users.context import menubar as menubar_users + +try: + from config import APP_NAME as ROOT_LOGGER_NAME +except ImportError: + ROOT_LOGGER_NAME = 'root' +logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__) + +ATTACHMENT_UID = 'attachment' +BACK_UID = 'back' +HELP_UID = 'help' + + +def context_adaption(context, request, **kwargs): + caller_name = inspect.currentframe().f_back.f_code.co_name + try: + context.set_additional_title(kwargs.pop('title')) + except KeyError: + pass # no title in kwargs + menubar_users(context[context.MENUBAR], request) + menubar(context, request, caller_name, **kwargs) + actionbar(context, request, caller_name, **kwargs) + navigationbar(context, request, caller_name, **kwargs) + for key in kwargs: + context[key] = kwargs[key] + logger.debug("context adapted: %s", repr(context)) + + +def navigationbar(context, request, caller_name, **kwargs): + bar = context[context.NAVIGATIONBAR] + add_back_menu(request, bar) + # TODO: Add the pages navigation, if source is pages + finalise_bar(request, bar) + + +def add_back_menu(request, bar): + bar.append_entry( + BACK_UID, # uid + _('Back'), # name + gray_icon_url(request, 'back.png'), # icon + 'javascript:history.back()', # url + True, # left + False # active + ) + + +def menubar(context, request, caller_name, **kwargs): + bar = context[context.MENUBAR] + # replace_profile(request, bar) + add_help_menu(request, bar) + # add_tasklist_menu(request, bar) + # add_filter_submenu(request, bar, VIEW_TASKLIST_UID) + # add_projectlist_menu(request, bar) + # add_printview_menu(request, bar) + finalise_bar(request, bar) + + +def add_help_menu(request, bar): + bar.append_entry( + HELP_UID, # uid + _('Help'), # name + color_icon_url(request, 'help.png'), # icon + pages.url_helpview(request, 'main'), # url + True, # left + False # active + ) + + +def actionbar(context, request, caller_name, **kwargs): + bar = context[context.ACTIONBAR] + if caller_name == 'pages': + # acc = acc_task(kwargs['task'], request.user) + # if acc.modify or acc.modify_limited: + # add_edittask_menu(request, bar, kwargs['task'].id) + # if acc.add_comments: + # add_newcomment_menu(request, bar, kwargs['task'].id) + add_manageupload_menu(request, bar, kwargs['upload_path']) + elif caller_name == 'helpview': + actionbar_add_help(context, request, **kwargs) + finalise_bar(request, bar) + + +def add_manageupload_menu(request, bar, upload_path): + bar.append_entry( + ATTACHMENT_UID, # uid + _("Attachments"), # name + color_icon_url(request, 'upload.png'), # icon + mycreole.url_manage_uploads(request, upload_path), # url + True, # left + False, # active + ) + + +def finalise_bar(request, bar): + if len(bar) == 0: + bar.append_entry(*empty_entry_parameters(request)) + + +"""from .access import create_project_possible, create_task_possible, acc_task +from django.db.models.functions import Lower +import patt +from .search import common_searches +from themes import empty_entry_parameters, color_icon_url, gray_icon_url +from users.context import PROFILE_ENTRY_UID + + +COMMENTNEW_UID = 'commentnew' +CREATE_PROJECT_UID = 'create-project' +CREATE_TASK_UID = 'create-task' +PRINTVIEW_UID = 'printview' +TASKEDIT_UID = 'taskedit' +VIEW_PROJECTLIST_UID = 'view-projectlist' +VIEW_TASKLIST_UID = 'view-tasklist' + + + + + + + + +def replace_profile(request, bar): + try: + bar.replace_entry( + PROFILE_ENTRY_UID, + PROFILE_ENTRY_UID, # uid + request.user.username, # name + color_icon_url(request, 'user.png'), # icon + patt.url_profile(request), # url + False, # left + False # active + ) + except ValueError: + pass # Profile entry does not exist, so exchange is not needed (e.g. no user is logged in) + + + + +def add_tasklist_menu(request, bar): + bar.append_entry( + VIEW_TASKLIST_UID, # uid + _('Tasklist'), # name + color_icon_url(request, 'task.png'), # icon + patt.url_tasklist(request), # url + True, # left + patt.is_tasklistview(request) # active + ) + + +def add_projectlist_menu(request, bar): + bar.append_entry( + VIEW_PROJECTLIST_UID, # uid + _('Projectlist'), # name + color_icon_url(request, 'folder.png'), # icon + patt.url_projectlist(request), # url + True, # left + patt.is_projectlistview(request) # active + ) + + +def add_printview_menu(request, bar): + bar.append_entry( + PRINTVIEW_UID, # uid + _('Printview'), # name + color_icon_url(request, 'print.png'), # icon + patt.url_printview(request), # url + True, # left + patt.is_printview(request) # active + ) + + +def add_newtask_menu(request, bar, project_id): + bar.append_entry( + CREATE_TASK_UID, # uid + _('New Task'), # name + color_icon_url(request, 'plus.png'), # icon + patt.url_tasknew(request, project_id), # url + True, # left + False # active + ) + + +def add_edittask_menu(request, bar, task_id): + bar.append_entry( + TASKEDIT_UID, # uid + _('Edit'), # name + color_icon_url(request, 'edit.png'), # icon + patt.url_taskedit(request, task_id), # url + True, # left + False # active + ) + + +def add_newcomment_menu(request, bar, task_id): + bar.append_entry( + COMMENTNEW_UID, # uid + _('Add Comment'), # name + color_icon_url(request, 'edit2.png'), # icon + patt.url_commentnew(request, task_id), # url + True, # left + False # active + ) + + +def add_newproject_menu(request, bar): + bar.append_entry( + CREATE_PROJECT_UID, # uid + _('New Project'), # name + color_icon_url(request, 'plus.png'), # icon + patt.url_projectnew(request), # url + True, # left + False # active + ) + + + + + + +def add_filter_submenu(request, bar, menu_uid): + bar.append_entry_to_entry( + menu_uid, + menu_uid + '-easysearch', # uid + _('Easysearch'), # name + gray_icon_url(request, 'search.png'), # icon + patt.url_easysearch(request), # url + True, # left + False # active + ) + if patt.get_search_query(request) is not None: + bar.append_entry_to_entry( + menu_uid, + menu_uid + '-save', # uid + _('Save Search as Filter'), # name + gray_icon_url(request, 'save.png'), # icon + patt.url_filteredit(request), # url + True, # left + False # active + ) + bar.append_entry_to_entry( + menu_uid, + menu_uid + '-all', # uid + _('All Tasks'), # name + gray_icon_url(request, 'task.png'), # icon + patt.url_tasklist(request), # url + True, # left + False # active + ) + cs = common_searches(request) + for common_filter_id in cs: + bar.append_entry_to_entry( + menu_uid, + menu_uid + '-common', # uid + _(cs[common_filter_id][0]), # name + gray_icon_url(request, 'filter.png'), # icon + patt.url_tasklist(request, common_filter_id=common_filter_id), # url + True, # left + False # active + ) + for s in request.user.search_set.order_by(Lower('name')): + active = patt.is_tasklistview(request, s.id) + if active is True: + url = patt.url_filteredit(request, s.id) + else: + url = patt.url_tasklist(request, user_filter_id=s.id) + if active: + icon = 'settings.png' + else: + icon = 'favourite.png' + bar.append_entry_to_entry( + menu_uid, + menu_uid + '-sub', # uid + s.name, # name + gray_icon_url(request, icon), # icon + url, # url + True, # left + active # active + ) +""" diff --git a/pages/creole.py b/pages/creole.py new file mode 100644 index 0000000..f0eb164 --- /dev/null +++ b/pages/creole.py @@ -0,0 +1,27 @@ +from django.urls.base import reverse + +# TODO: Add a filter to show all subpages <> and add it to settings and help + + +""" +def page_link_filter(text): + render_txt = '' + while len(text) > 0: + try: + pos = text.index('[[page:') + except ValueError: + pos = len(text) + print(pos) + render_txt += text[:pos] + text = text[pos + 7:] + if len(text): + pos = text.index(']]') + try: + rel_path = int(text[:pos]) + except ValueError: + render_txt += "[[page:" + text[:pos + 2] + else: + render_txt += '[[%s|%s]]' % (reverse('pages-pages', kwargs={'rel_path': rel_path}), rel_path) + text = text[pos + 2:] + return render_txt +""" diff --git a/pages/help.py b/pages/help.py new file mode 100644 index 0000000..d9584c3 --- /dev/null +++ b/pages/help.py @@ -0,0 +1,58 @@ +from django.utils.translation import gettext as _ +import mycreole +import pages +from themes import color_icon_url + + +HELP_UID = 'help' + +MAIN = mycreole.render_simple(_( + """ += Piki + +**piki** is a minimal wiki implemented with python and django. + +== Help +* [[creole|Creole Markup Language]] +* [[access|Access Control for the site content]] +* [[search|Help on Search]] +""")) + +CREOLE = mycreole.mycreole_help_pagecontent() +CREOLE += mycreole.render_simple(""" += Piki Markup +{{{[[rel_path_to_page|Name]]}}} will result in a Link to the given wiki page. +""") + +ACCESS = mycreole.render_simple(_(""" += TBD +""")) + +SEARCH = mycreole.render_simple(_(""" += TBD +""")) + +help_pages = { + 'main': MAIN, + 'creole': CREOLE, + 'access': ACCESS, + 'search': SEARCH, +} + + +def actionbar(context, request, current_help_page=None, **kwargs): + actionbar_entries = ( + ('1', 'Main'), + ('2', 'Creole'), + ('3', 'Access'), + ('4', 'Search'), + ) + for num, name in actionbar_entries: + context[context.ACTIONBAR].append_entry( + HELP_UID + '-%s' % name.lower(), # uid + _(name), # name + color_icon_url(request, num + '.png'), # icon + pages.url_helpview(request, name.lower()), # url + True, # left + name.lower() == current_help_page, # active + ) diff --git a/pages/migrations/__init__.py b/pages/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pages/models.py b/pages/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/pages/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/pages/page.py b/pages/page.py new file mode 100644 index 0000000..27354a4 --- /dev/null +++ b/pages/page.py @@ -0,0 +1,47 @@ +from django.conf import settings + +import mycreole +import os + + +class page(object): + SPLITCHAR = ":" + FOLDER_ATTACHMENTS = "attachments" + FOLDER_CONTENT = 'content' + FILE_NAME = 'page' + + def __init__(self, rel_path) -> None: + self._rel_path = rel_path + + def rel_path_is_valid(self): + return not self.SPLITCHAR in self._rel_path + + def is_available(self): + return os.path.isfile(self.content_file_name) + + @property + def title(self): + return os.path.basename(self._rel_path) + + @property + def attachment_path(self): + return os.path.join(self.content_folder_name, self.FOLDER_ATTACHMENTS) + + @property + def content_folder_name(self): + return self._rel_path.replace('/', '::') + + @property + def content_file_name(self): + return os.path.join(settings.PAGES_ROOT, self.content_folder_name, self.FOLDER_CONTENT, self.FILE_NAME) + + def __read_content__(self): + if self.is_available(): + with open(self.content_file_name, 'r') as fh: + return fh.read() + else: + # TODO: Create message for creation or no content dependent of user has write access + return "Page not available. Create it." + + def render_to_html(self, request): + return mycreole.render(request, self.__read_content__(), self.attachment_path, "next_anchor") diff --git a/pages/templates/pages/page.html b/pages/templates/pages/page.html new file mode 100644 index 0000000..ee8d943 --- /dev/null +++ b/pages/templates/pages/page.html @@ -0,0 +1,5 @@ +{% extends "themes/"|add:settings.page_theme|add:"/base.html" %} + +{% block content %} +{{ page_content|safe }} +{% endblock content %} diff --git a/pages/tests.py b/pages/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/pages/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/pages/views.py b/pages/views.py new file mode 100644 index 0000000..6650bab --- /dev/null +++ b/pages/views.py @@ -0,0 +1,56 @@ +from django.shortcuts import render +from django.http import HttpResponse, HttpResponseRedirect +from django.utils.translation import gettext as _ + +import logging + +import config +from . import url_page +from .context import context_adaption +from .help import help_pages +from .page import page +from themes import Context + +logger = logging.getLogger(__name__) + + +def root(request): + return HttpResponseRedirect(url_page(request, config.STARTPAGE)) + + +def pages(request, rel_path=''): + context = Context(request) # needs to be executed first because of time mesurement + # + p = page(rel_path) + # + context_adaption( + context, + request, + title=p.title, + upload_path=p.attachment_path, + page_content=p.render_to_html(request) + ) + return render(request, 'pages/page.html', context=context) + + +def search(request): + context = Context(request) # needs to be executed first because of time mesurement + context_adaption( + context, + request, + page_content="Search is not yet implemented..." + ) + return render(request, 'pages/page.html', context=context) + + +def helpview(request, page='main'): + context = Context(request) # needs to be executed first because of time mesurement + page_content = help_pages[page] + context_adaption( + context, # the base context + request, # the request object to be used in context_adaption + current_help_page=page, # the current help_page to identify which taskbar entry has to be highlighted + page_content=page_content, # the help content itself (template) + title=_('Help') # the title for the page (template) + ) + return render(request, 'pages/page.html', context=context) diff --git a/piki/__init__.py b/piki/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/piki/asgi.py b/piki/asgi.py new file mode 100644 index 0000000..1f8f2d5 --- /dev/null +++ b/piki/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for piki project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'piki.settings') + +application = get_asgi_application() diff --git a/piki/settings.py b/piki/settings.py new file mode 100644 index 0000000..f53121c --- /dev/null +++ b/piki/settings.py @@ -0,0 +1,178 @@ +""" +Django settings for piki project. + +Generated by 'django-admin startproject' using Django 5.1.1. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +import config +from config import APP_NAME as ROOT_LOGGER_NAME +import os +from pathlib import Path +import random +import stat +import sys + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Check permission of config.py +# +if sys.platform == 'linux' or sys.platform == 'linux2': + st = os.stat(os.path.join(BASE_DIR, 'config.py')) + if st.st_mode & stat.S_IRGRP or st.st_mode & stat.S_IROTH: + raise PermissionError("conig.py is readable by group or others.") + +# Default values, if not defined in config.py +# +USER_CONFIG_DEFAULTS = { + 'DEBUG': False, + 'SECRET_KEY': None, + 'DEFAULT_THEME': 'clear-blue', + 'ALLOWED_HOSTS': ['127.0.0.1', 'localhost', ], + 'CSRF_TRUSTED_ORIGINS': [], +} + +# Set configuration parameters +# +thismodule = sys.modules[__name__] +for property_name in USER_CONFIG_DEFAULTS: + try: + value = getattr(config, property_name) + except AttributeError: + value = USER_CONFIG_DEFAULTS[property_name] + setattr(thismodule, property_name, value) + +# SECURITY WARNING: keep the secret key used in production secret! +# +if SECRET_KEY is None: + chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' + s_key = ''.join([random.choice(chars) for n in range(50)]) + secret_key_warning = "You need to create a config.py file including at least a SECRET_KEY definition (e.g.: --> %s <--). " % repr(s_key) + raise KeyError(secret_key_warning) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + + +# Application definition + +INSTALLED_APPS = [ + 'pages.apps.PagesConfig', + 'themes.apps.ThemesConfig', + 'mycreole.apps.MycreoleConfig', + 'users.apps.UsersConfig', + # + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'piki.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'piki.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_ROOT = os.path.join(BASE_DIR, 'data', 'static') +STATIC_URL = 'static/' + +MEDIA_ROOT = os.path.join(BASE_DIR, 'data', 'media') +MEDIA_URL = '/media/' + +PAGES_ROOT = os.path.join(BASE_DIR, 'data', 'pages') + +MYCREOLE_ROOT = os.path.join(BASE_DIR, 'data', 'pages') +MYCREOLE_ATTACHMENT_ACCESS = { + 'read': 'pages.access.read_attachment', + 'modify': 'pages.access.modify_attachment', +} +MYCREOLE_BAR = { + 'navibar': 'pages.context.navigationbar', + 'menubar': 'pages.context.menubar', +} +MYCREOLE_EXT_FILTERS = [ + # 'pages.creole.page_link_filter', +] +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/piki/urls.py b/piki/urls.py new file mode 100644 index 0000000..9a2cb10 --- /dev/null +++ b/piki/urls.py @@ -0,0 +1,38 @@ +""" +URL configuration for piki project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import path, include + +import pages.views + +urlpatterns = [ + path('admin/', admin.site.urls), + # + path('', pages.views.root, name='pages-root'), + path('pages/', pages.views.root, name='pages-root'), + path('pages/', pages.views.pages, name='pages-pages'), + path('helpview/', pages.views.helpview, name='pages-helpview'), + path('helpview/', pages.views.helpview, name='pages-helpview'), + path('search/', pages.views.search, name='search'), + path('mycreole/', include('mycreole.urls')), + path('users/', include('users.urls')), +] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/piki/wsgi.py b/piki/wsgi.py new file mode 100644 index 0000000..65ecb94 --- /dev/null +++ b/piki/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for piki project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'piki.settings') + +application = get_wsgi_application() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..684b4cb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +Django +Pillow +python-creole +pytz + diff --git a/themes b/themes new file mode 160000 index 0000000..13a8d8e --- /dev/null +++ b/themes @@ -0,0 +1 @@ +Subproject commit 13a8d8ebc4c82d2000af4464b863cf3b38a7a1fe diff --git a/users b/users new file mode 160000 index 0000000..0827a53 --- /dev/null +++ b/users @@ -0,0 +1 @@ +Subproject commit 0827a5311fdbbef689365c6db5762881e048ba9c