Page data moved to db. Prep for access control

This commit is contained in:
Dirk Alders 2024-10-21 07:20:37 +02:00
parent 292631ed21
commit a528ac19cb
20 changed files with 683 additions and 356 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
# piki # piki
data/media data/media
data/pages data/mycreole
data/static data/static
data/whoosh data/whoosh
db.sqlite3 db.sqlite3

View File

@ -1,5 +0,0 @@
{
"creation_time": 1728465254,
"modified_time": 1728465254,
"modified_user": "system-page"
}

View File

@ -1,2 +0,0 @@
= Index
<<allpages>>

View File

@ -1,5 +0,0 @@
{
"creation_time": 1728465495,
"modified_time": 1728649989,
"modified_user": "system-page"
}

View File

@ -1,5 +0,0 @@
= Default startpage
Edit this page to get your own first startpage.
If you need need assistance to edit a page, visit the [[/helpview/main|help pages]].

View File

@ -1,5 +0,0 @@
{
"modified_time": 1729022206,
"modified_user": "system-page",
"creation_time": 1729022206
}

View File

@ -1,2 +0,0 @@
= Tree
<<allpagestree>>

View File

@ -1,16 +1,27 @@
def read_page(request, rel_path): class access_control(object):
return "private" not in rel_path or write_page(request, rel_path) def __init__(self, request, rel_path):
self._request = request
self._rel_path = rel_path
def may_read(self):
return "private" not in self._rel_path or self.may_write()
def write_page(request, rel_path): def may_write(self):
return request.user.is_authenticated and request.user.username in ['root', 'dirk'] # /!\ rel_path is the filsystem rel_path - caused by the flat folder structure /!\
return self._request.user.is_authenticated and self._request.user.username in ['root', 'dirk']
def may_read_attachment(self):
return self.may_read()
def may_modify_attachment(self):
return self.may_write()
def read_attachment(request, rel_path): def read_attachment(request, rel_path):
# /!\ rel_path is the filsystem rel_path - caused by the flat folder structure /!\ # Interface for external module mycreole
return True return access_control(request, rel_path).may_read_attachment()
def modify_attachment(request, rel_path): def modify_attachment(request, rel_path):
# /!\ rel_path is the filsystem rel_path - caused by the flat folder structure /!\ # Interface for external module mycreole
return request.user.is_authenticated and request.user.username in ['root', 'dirk'] return access_control(request, rel_path).may_modify_attachment()

View File

@ -1,3 +1,17 @@
from django.contrib import admin from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin
# Register your models here. from .models import PikiPage
class PikiPageAdmin(SimpleHistoryAdmin):
list_display = ('rel_path', 'tags', 'deleted')
history_list_display = ('rel_path', 'tags', 'deleted')
search_fields = ('rel_path', 'tags', )
list_filter = (
('deleted', admin.BooleanFieldListFilter),
)
ordering = ["rel_path"]
admin.site.register(PikiPage, PikiPageAdmin)

View File

@ -5,7 +5,7 @@ import os
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from pages import access from pages.access import access_control
import pages.parameter import pages.parameter
from .help import actionbar as actionbar_add_help from .help import actionbar as actionbar_add_help
import mycreole import mycreole
@ -74,11 +74,12 @@ def actionbar(context, request, caller_name, **kwargs):
bar = context[context.ACTIONBAR] bar = context[context.ACTIONBAR]
if not cms_mode_active(request): if not cms_mode_active(request):
if caller_name in ['page', 'edit', 'delete', 'rename']: if caller_name in ['page', 'edit', 'delete', 'rename']:
if access.write_page(request, kwargs["rel_path"]): acc = access_control(request, kwargs["rel_path"])
if acc.may_write():
add_page_menu(request, bar, kwargs["rel_path"], kwargs.get('is_available', False)) add_page_menu(request, bar, kwargs["rel_path"], kwargs.get('is_available', False))
if access.modify_attachment(request, kwargs["rel_path"]): if acc.may_modify_attachment():
add_manageupload_menu(request, bar, kwargs['upload_path'], kwargs.get('is_available', False)) add_manageupload_menu(request, bar, kwargs['upload_path'], kwargs.get('is_available', False))
if access.read_page(request, kwargs["rel_path"]): if acc.may_read():
add_meta_menu(request, bar, kwargs["rel_path"], kwargs.get('is_available', False)) add_meta_menu(request, bar, kwargs["rel_path"], kwargs.get('is_available', False))
elif caller_name == 'helpview': elif caller_name == 'helpview':
actionbar_add_help(context, request, **kwargs) actionbar_add_help(context, request, **kwargs)

View File

@ -3,17 +3,13 @@ from django import forms
from django.forms.renderers import BaseRenderer from django.forms.renderers import BaseRenderer
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from .models import PikiPage
class EditForm(forms.Form): # Note that it is not inheriting from forms.ModelForm
page_txt = forms.CharField(max_length=20000, label="Page source text", widget=forms.Textarea(attrs={"rows": "20"}))
page_tags = forms.CharField(max_length=500, label="Tags (words separated by spaces)", required=False)
def __init__(self, *args, **kwargs) -> None: class EditForm(forms.ModelForm):
page_data = kwargs.pop("page_data") class Meta:
page_tags = kwargs.pop("page_tags") model = PikiPage
super().__init__(*args, **kwargs) fields = ["page_txt", "tags", "owner", "group"]
self.fields['page_txt'].initial = page_data
self.fields['page_tags'].initial = page_tags
class RenameForm(forms.Form): # Note that it is not inheriting from forms.ModelForm class RenameForm(forms.Form): # Note that it is not inheriting from forms.ModelForm

View File

@ -0,0 +1,57 @@
from django.conf import settings
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from pages.models import PikiPage
from datetime import datetime
import fstools
from zoneinfo import ZoneInfo
SYSTEM_PAGES = {
"tree": """= Tree
<<allpagestree>>""",
"index": """= Index
<<allpages>>""",
}
def add_page_data(rel_path, tags, page_txt, creation_time, creation_user, modified_time, modified_user):
try:
page = PikiPage.objects.get(rel_path=rel_path)
except PikiPage.DoesNotExist:
page = PikiPage(rel_path=rel_path)
#
page.tags = tags
page.page_txt = page_txt
#
page.creation_time = creation_time
try:
page.creation_user = User.objects.get(username=creation_user)
except User.DoesNotExist:
page.creation_user = None
page.modified_time = modified_time
try:
page.modified_user = User.objects.get(username=modified_user)
except User.DoesNotExist:
page.modified_user = None
#
page.save()
class Command(BaseCommand):
def handle(self, *args, **options):
for rel_path in SYSTEM_PAGES:
self.stdout.write(self.style.MIGRATE_HEADING("Migration of page '%s'" % rel_path))
#
dtm = datetime.now(ZoneInfo("UTC"))
add_page_data(
rel_path,
"",
SYSTEM_PAGES[rel_path],
dtm,
None,
dtm,
None
)

View File

@ -0,0 +1,73 @@
from django.conf import settings
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from pages.page import full_path_all_pages, page_wrapped
from pages.models import PikiPage
from datetime import datetime
import fstools
import os
import shutil
from zoneinfo import ZoneInfo
def add_page_data(rel_path, tags, page_txt, creation_time, creation_user, modified_time, modified_user):
try:
page = PikiPage.objects.get(rel_path=rel_path)
except PikiPage.DoesNotExist:
page = PikiPage(rel_path=rel_path)
#
page.tags = tags
page.page_txt = page_txt
#
page.creation_time = datetime.fromtimestamp(creation_time, ZoneInfo("UTC"))
creation_user = creation_user or "dirk"
page.creation_user = User.objects.get(username=creation_user)
modified_user = modified_user or "dirk"
page.modified_time = datetime.fromtimestamp(modified_time, ZoneInfo("UTC"))
page.modified_user = User.objects.get(username=modified_user)
page.owner = page.owner or page.creation_user
#
page.save()
class Command(BaseCommand):
def handle(self, *args, **options):
for path in full_path_all_pages():
fs_page = page_wrapped(None, path)
if fs_page._page.is_available():
self.stdout.write(self.style.MIGRATE_HEADING("Migration of page '%s'" % fs_page.rel_path))
for history_number in fs_page._page.history_numbers_list():
self.stdout.write(self.style.MIGRATE_HEADING(" * Adding history version %d" % history_number))
h_page = page_wrapped(None, path, history_version=history_number)
add_page_data(
rel_path=h_page.rel_path,
tags=h_page.tags,
page_txt=h_page._page.raw_page_src,
#
creation_time=h_page.creation_time,
creation_user=h_page.creation_user,
modified_time=h_page.modified_time,
modified_user=h_page.modified_user
)
#
self.stdout.write(self.style.MIGRATE_HEADING(" * Adding current version"))
add_page_data(
rel_path=fs_page.rel_path,
tags=fs_page.tags,
page_txt=fs_page._page.raw_page_src,
#
creation_time=fs_page.creation_time,
creation_user=fs_page.creation_user,
modified_time=fs_page.modified_time,
modified_user=fs_page.modified_user
)
#
src = os.path.join(path, "attachments")
if os.path.isdir(src):
dst = os.path.join(settings.MYCREOLE_ROOT, fs_page.rel_path)
for attachment in fstools.filelist(src):
self.stdout.write(self.style.MIGRATE_HEADING(" * Copy attachment ''%s to new location" % os.path.basename(attachment)))
fstools.mkdir(dst)
shutil.copy(attachment, dst)

View File

@ -8,6 +8,11 @@ def permission_denied_msg_page(request, rel_path):
messages.error(request, _("Permission denied: You don't have sufficient acces to the Page '%s'. Please contact the adminstrator.") % rel_path) messages.error(request, _("Permission denied: You don't have sufficient acces to the Page '%s'. Please contact the adminstrator.") % rel_path)
def deleted_page(request):
# TODO: Add translation for this message
messages.info(request, _("Page was deleted. Recover not yet implemented. Use the 'Administration' area for recovery. Rebuild the search index afterwards."))
def unavailable_msg_page(request, rel_path): def unavailable_msg_page(request, rel_path):
# TODO: Add translation for this message # TODO: Add translation for this message
messages.info(request, _("Unavailable: The Page '%s' is not available. Create it or follow a valid link, please.") % rel_path) messages.info(request, _("Unavailable: The Page '%s' is not available. Create it or follow a valid link, please.") % rel_path)
@ -38,6 +43,11 @@ def page_renamed(request):
messages.info(request, _('The page has been renamed.')) messages.info(request, _('The page has been renamed.'))
def internal_error(request):
# TODO: Add translation for this message
messages.error(request, _('internal ERROR: Action not performed..'))
def history_version_display(request, rel_path, history_version): def history_version_display(request, rel_path, history_version):
# TODO: Add translation for this message # TODO: Add translation for this message
messages.warning(request, _("You see an old version of the page (Version = %d). Click <a href='%s'>here</a> to recover this Version.") % ( messages.warning(request, _("You see an old version of the page (Version = %d). Click <a href='%s'>here</a> to recover this Version.") % (

View File

@ -0,0 +1,63 @@
# Generated by Django 5.1.2 on 2024-10-21 04:20
import django.db.models.deletion
import simple_history.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='HistoricalPikiPage',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('rel_path', models.CharField(db_index=True, max_length=1000)),
('page_txt', models.TextField(max_length=50000)),
('tags', models.CharField(blank=True, max_length=1000, null=True)),
('deleted', models.BooleanField(default=False)),
('creation_time', models.DateTimeField(blank=True, null=True)),
('modified_time', models.DateTimeField(blank=True, null=True)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('creation_user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
('group', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='auth.group')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('modified_user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
('owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical piki page',
'verbose_name_plural': 'historical piki pages',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='PikiPage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('rel_path', models.CharField(max_length=1000, unique=True)),
('page_txt', models.TextField(max_length=50000)),
('tags', models.CharField(blank=True, max_length=1000, null=True)),
('deleted', models.BooleanField(default=False)),
('creation_time', models.DateTimeField(blank=True, null=True)),
('modified_time', models.DateTimeField(blank=True, null=True)),
('creation_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='creation_user', to=settings.AUTH_USER_MODEL)),
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='group', to='auth.group')),
('modified_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_user', to=settings.AUTH_USER_MODEL)),
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='owner', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -1,3 +1,284 @@
from django.conf import settings
from django.contrib.auth.models import User, Group
from django.db import models from django.db import models
from django.utils.translation import gettext as _
from simple_history.models import HistoricalRecords
# Create your models here. from datetime import datetime
import difflib
import logging
import os
from zoneinfo import ZoneInfo
from users.models import get_userprofile
from pages import url_page
import mycreole
logger = logging.getLogger(settings.ROOT_LOGGER_NAME).getChild(__name__)
class PikiPage(models.Model):
SAVE_ON_CHANGE_FIELDS = ["rel_path", "page_txt", "tags", "deleted", "owner", "group"]
#
rel_path = models.CharField(unique=True, max_length=1000)
page_txt = models.TextField(max_length=50000)
tags = models.CharField(max_length=1000, null=True, blank=True)
deleted = models.BooleanField(default=False)
#
creation_time = models.DateTimeField(null=True, blank=True)
creation_user = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name="creation_user")
modified_time = models.DateTimeField(null=True, blank=True)
modified_user = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name="modified_user")
#
owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name="owner")
group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL, related_name="group")
# owner_perms
# group_perms
# other_perms
#
history = HistoricalRecords()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def prepare_save(self, request):
# Set date
tmd = datetime.now(tz=ZoneInfo("UTC")).replace(microsecond=0)
self.creation_time = self.creation_time or tmd
self.modified_time = tmd
# Set user
self.creation_user = self.creation_user or request.user
self.owner = self.owner or request.user
self.modified_user = request.user
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
if self.id and not force_update:
orig = PikiPage.objects.get(id=self.id)
for key in self.SAVE_ON_CHANGE_FIELDS:
if getattr(self, key) != getattr(orig, key):
break
else:
self.save_needed = False
return False
self.save_needed = True
return models.Model.save(self, force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
#
# Set history datetime to modified datetime
#
@property
def _history_date(self):
return self.modified_time
@_history_date.setter
def _history_date(self, value):
self.modified_time = value
#
# My information
#
@property
def title(self):
return self.rel_path.split("/")[-1]
#
# My methods
#
def render_to_html(self, request, history=None):
if history:
h = self.history.get(history_id=history)
return self.render_text(request, h.page_txt)
else:
return self.render_text(request, self.page_txt)
def user_datetime(self, request, dtm):
try:
up = get_userprofile(request.user)
except AttributeError:
tz = ZoneInfo("UTC")
else:
tz = ZoneInfo(up.timezone)
#
return datetime.astimezone(dtm, tz)
def render_meta(self, request, history):
# Page information
meta = f'= {_("Meta data")}\n'
meta += f'|{_("Created by")}:|{self.creation_user}|\n'
meta += f'|{_("Created at")}:|{self.user_datetime(request, self.creation_time)}|\n'
meta += f'|{_("Modified by")}:|{self.modified_user}|\n'
meta += f'|{_("Modified at")}:|{self.user_datetime(request, self.modified_time)}|\n'
meta += f'|{_("Owner")}:|{self.owner or "---"}|\n'
meta += f'|{_("Group")}:|{self.group or "---"}|\n'
meta += f'|{_("Tags")}|{self.tags or "---"}|\n'
#
# List of history page versions
#
hl = self.history.all()[1:]
if len(hl) > 0:
meta += f'= {_("History")}\n'
meta += f'| ={_("Version")} | ={_("Date")} | ={_("Page")} | ={_("Meta data")} | ={_("Page changed")} | ={_("Tags changed")} | \n'
# Current
name = _("Current")
meta += f"| {name} \
| {self.user_datetime(request, self.modified_time)} \
| [[{url_page(self.rel_path)} | Page]] \
| [[{url_page(self.rel_path, meta=None)} | Meta]] |"
page_content = self.page_txt.replace("\r\n", "\n").strip("\n")
tags = self.tags
for h_page in hl:
page_changed = page_content != h_page.page_txt.replace("\r\n", "\n").strip("\n")
tags_changed = tags != h_page.tags
if page_changed or tags_changed:
meta += " %s |" % ("Yes" if page_changed else "No")
meta += " %s |" % ("Yes" if tags_changed else "No")
meta += "\n"
meta += f"| {h_page.history_id} \
| {self.user_datetime(request, h_page.modified_time)} \
| [[{url_page(self.rel_path, history=h_page.history_id)} | Page]] \
| [[{url_page(self.rel_path, meta=None, history=h_page.history_id)} | Meta]] (with diff to current) |"
page_content = h_page.page_txt[:].replace("\r\n", "\n").strip("\n")
tags = h_page.tags
meta += " --- | --- |\n"
# Diff
html_diff = ""
if history:
h_page = self.history.get(history_id=history)
#
meta += f'= {_("Page differences")}\n'
#
left_lines = self.page_txt.splitlines()
right_lines = h_page.page_txt.splitlines()
html_diff = difflib.HtmlDiff(wrapcolumn=80).make_table(left_lines, right_lines, "Current page", "Page Version %d" % history)
#
return mycreole.render_simple(meta) + html_diff
#
# Creole stuff
#
def render_text(self, request, txt):
macros = {
"subpages": self.macro_subpages,
"allpages": self.macro_allpages,
"subpagetree": self.macro_subpagetree,
"allpagestree": self.macro_allpagestree,
}
return mycreole.render(request, txt, self.rel_path, macros=macros)
def macro_subpages(self, *args, **kwargs):
return self.macro_pages(*args, **kwargs)
def macro_allpages(self, *args, **kwargs):
kwargs["allpages"] = True
return self.macro_pages(*args, **kwargs)
def macro_allpagestree(self, *args, **kwargs):
kwargs["allpages"] = True
kwargs["tree"] = True
return self.macro_pages(*args, **kwargs)
def macro_subpagetree(self, * args, **kwargs):
kwargs["tree"] = True
return self.macro_pages(*args, **kwargs)
def macro_pages(self, *args, **kwargs):
allpages = kwargs.pop("allpages", False)
tree = kwargs.pop("tree", False)
#
def parse_depth(s: str):
try:
return int(s)
except ValueError:
pass
params = kwargs.get('', '')
filter_str = ''
depth = parse_depth(params)
if depth is None:
params = params.split(",")
depth = parse_depth(params[0])
if len(params) == 2:
filter_str = params[1]
elif depth is None:
filter_str = params[0]
#
if not allpages:
filter_str = os.path.join(self.rel_path, filter_str)
#
pages = PikiPage.objects.filter(rel_path__contains=filter_str)
pl = page_list([p for p in pages if not p.deleted])
#
if tree:
return "<pre>\n" + page_tree(pl).html() + "</pre>\n"
else:
return pl.html_list(depth=depth, filter_str=filter_str, parent_rel_path='' if allpages else self.rel_path)
class page_list(list):
def __init__(self, *args, **kwargs):
return super().__init__(*args, **kwargs)
def sort_basename(self):
return list.sort(self, key=lambda x: os.path.basename(x.rel_path))
def creole_list(self, depth=None, filter_str='', parent_rel_path=''):
self.sort_basename()
depth = depth or 9999 # set a random high value if None
#
rv = ""
last_char = None
for page in self:
if page.rel_path.startswith(filter_str) and page.rel_path != filter_str:
name = page.rel_path[len(parent_rel_path):].lstrip("/")
if name.count('/') < depth:
first_char = os.path.basename(name)[0].upper()
if last_char != first_char:
last_char = first_char
rv += f"=== {first_char}\n"
rv += f"* [[{url_page(page.rel_path)} | {name} ]]\n"
return rv
def html_list(self, depth=9999, filter_str='', parent_rel_path=''):
return mycreole.render_simple(self.creole_list(depth, filter_str, parent_rel_path))
class page_tree(dict):
T_PATTERN = "├── "
L_PATTERN = "└── "
I_PATTERN = ""
D_PATTERN = " "
def __init__(self, pl: page_list):
super().__init__()
for page in pl:
store_item = self
for entry in page.rel_path.split("/"):
if not entry in store_item:
store_item[entry] = {}
store_item = store_item[entry]
def html(self, rel_path=None, fill=""):
base = self
try:
for key in rel_path.split("/"):
base = base[key]
except AttributeError:
rel_path = ''
#
rv = ""
#
l = len(base)
for entry in sorted(list(base.keys())):
l -= 1
page_path = os.path.join(rel_path, entry)
try:
PikiPage.objects.get(rel_path=page_path)
except PikiPage.DoesNotExist:
pass
else:
entry = f'<a href="{url_page(page_path)}">{entry}</a>'
rv += fill + (self.L_PATTERN if l == 0 else self.T_PATTERN) + entry + "\n"
rv += self.html(page_path, fill=fill+(self.D_PATTERN if l == 0 else self.I_PATTERN))
return rv

View File

@ -30,22 +30,27 @@ def full_path_all_pages(expression="*"):
return rv return rv
def full_path_deleted_pages(expression="*"): class base(dict):
system_pages = fstools.dirlist(settings.SYSTEM_PAGES_ROOT, expression=expression, rekursive=False) @property
system_pages = [os.path.join(settings.PAGES_ROOT, os.path.basename(path)) for path in system_pages] def rel_path(self):
pages = fstools.dirlist(settings.PAGES_ROOT, expression=expression, rekursive=False) return os.path.basename(self._path).replace(2*SPLITCHAR, "/")
rv = []
for path in set(system_pages + pages): def is_available(self):
p = page_wrapped(None, path) is_a = os.path.isfile(self.filename)
if not p.is_available(): if not is_a:
rv.append(path) logger.debug("Not available - %s", self.filename)
return rv return is_a
def history_numbers_list(self):
history_folder = os.path.join(self._path, HISTORY_FOLDER_NAME)
return list(set([int(os.path.basename(filename)[:5]) for filename in fstools.filelist(history_folder)]))
class meta_data(dict): class meta_data(base):
META_FILE_NAME = 'meta.json' META_FILE_NAME = 'meta.json'
# #
KEY_CREATION_TIME = "creation_time" KEY_CREATION_TIME = "creation_time"
KEY_CREATION_USER = "creation_user"
KEY_MODIFIED_TIME = "modified_time" KEY_MODIFIED_TIME = "modified_time"
KEY_MODIFIED_USER = "modified_user" KEY_MODIFIED_USER = "modified_user"
KEY_TAGS = "tags" KEY_TAGS = "tags"
@ -85,6 +90,9 @@ class meta_data(dict):
if username: if username:
self[self.KEY_MODIFIED_TIME] = int(time.time()) self[self.KEY_MODIFIED_TIME] = int(time.time())
self[self.KEY_MODIFIED_USER] = username self[self.KEY_MODIFIED_USER] = username
#
if self.KEY_CREATION_USER not in self:
self[self.KEY_CREATION_USER] = self[self.KEY_MODIFIED_USER]
if self.KEY_CREATION_TIME not in self: if self.KEY_CREATION_TIME not in self:
self[self.KEY_CREATION_TIME] = self[self.KEY_MODIFIED_TIME] self[self.KEY_CREATION_TIME] = self[self.KEY_MODIFIED_TIME]
if tags: if tags:
@ -109,7 +117,7 @@ class meta_data(dict):
shutil.copy(self.filename, history_filename) shutil.copy(self.filename, history_filename)
class page_data(object): class page_data(base):
PAGE_FILE_NAME = 'page' PAGE_FILE_NAME = 'page'
def __init__(self, path, history_version=None): def __init__(self, path, history_version=None):
@ -168,12 +176,6 @@ class page_data(object):
def rel_path(self): def rel_path(self):
return os.path.basename(self._path).replace(2*SPLITCHAR, "/") return os.path.basename(self._path).replace(2*SPLITCHAR, "/")
def is_available(self):
is_a = os.path.isfile(self.filename)
if not is_a:
logger.debug("page.is_available: Not available - %s", self.filename)
return is_a
@property @property
def title(self): def title(self):
return os.path.basename(self._path).split(2*SPLITCHAR)[-1] return os.path.basename(self._path).split(2*SPLITCHAR)[-1]
@ -189,200 +191,6 @@ class page_data(object):
shutil.copy(self.filename, history_filename) shutil.copy(self.filename, history_filename)
class page_django(page_data):
FOLDER_ATTACHMENTS = "attachments"
def __init__(self, request, path, history_version=None) -> None:
self._request = request
super().__init__(path, history_version=history_version)
@property
def attachment_path(self):
return os.path.join(os.path.basename(self._path), self.FOLDER_ATTACHMENTS)
def render_to_html(self):
if self.is_available():
return self.render_text(self._request, self.raw_page_src)
else:
messages.unavailable_msg_page(self._request, self.rel_path)
return ""
def history_numbers_list(self):
history_folder = os.path.join(self._path, HISTORY_FOLDER_NAME)
return list(set([int(os.path.basename(filename)[:5]) for filename in fstools.filelist(history_folder)]))
def render_meta(self, ctime, mtime, user, tags):
#
# Page meta data
#
meta = f'=== {_("Meta data")}\n'
meta += f'|{_("Created")}:|{timestamp_to_datetime(self._request, ctime)}|\n'
meta += f'|{_("Modified")}:|{timestamp_to_datetime(self._request, mtime)}|\n'
meta += f'|{_("Editor")}|{user}|\n'
meta += f'|{_("Tags")}|{tags}|\n'
#
# List of hostory page versions
#
hnl = self.history_numbers_list()
if hnl:
meta += f'=== {_("History")}\n'
meta += f'| ={_("Version")} | ={_("Date")} | ={_("Page")} | ={_("Meta data")} | \n'
# Current
name = _("Current")
meta += f"| {name} \
| {timestamp_to_datetime(self._request, mtime)} \
| [[{url_page(self.rel_path)} | Page]] \
| [[{url_page(self.rel_path, meta=None)} | Meta]]\n"
# History
for num in reversed(hnl):
p = page_wrapped(self._request, self._path, history_version=num)
meta += f"| {num} \
| {timestamp_to_datetime(self._request, p.modified_time)} \
| [[{url_page(p.rel_path, history=num)} | Page]] \
| [[{url_page(p.rel_path, meta=None, history=num)} | Meta]] (with page changes)\n"
# Diff
html_diff = ""
if self._history_version:
meta += f'=== {_("Page differences")}\n'
#
c = page_django(self._request, self._path)
left_lines = c.raw_page_src.splitlines()
right_lines = self.raw_page_src.splitlines()
html_diff = difflib.HtmlDiff(wrapcolumn=80).make_table(left_lines, right_lines)
#
return mycreole.render_simple(meta) + html_diff
def render_text(self, request, txt):
macros = {
"subpages": self.macro_subpages,
"allpages": self.macro_allpages,
"subpagetree": self.macro_subpagetree,
"allpagestree": self.macro_allpagestree,
}
return mycreole.render(request, txt, self.attachment_path, macros=macros)
def macro_allpages(self, *args, **kwargs):
kwargs["allpages"] = True
return self.macro_subpages(*args, **kwargs)
def macro_subpages(self, *args, **kwargs):
allpages = kwargs.pop("allpages", False)
tree = kwargs.pop("tree", False)
#
def parse_depth(s: str):
try:
return int(s)
except ValueError:
pass
params = kwargs.get('', '')
filter_str = ''
depth = parse_depth(params)
if depth is None:
params = params.split(",")
depth = parse_depth(params[0])
if len(params) == 2:
filter_str = params[1]
elif depth is None:
filter_str = params[0]
#
rv = ""
# create a page_list
if allpages:
expression = "*"
parent_rel_path = ""
else:
expression = os.path.basename(self._path) + 2 * SPLITCHAR + "*"
parent_rel_path = self.rel_path
#
pl = page_list(
self._request,
[page_django(self._request, path) for path in full_path_all_pages(expression)]
)
if tree:
return "<pre>\n" + page_tree(pl).html() + "</pre>\n"
else:
return pl.html_list(depth=depth, filter_str=filter_str, parent_rel_path=parent_rel_path)
def macro_allpagestree(self, *args, **kwargs):
kwargs["allpages"] = True
kwargs["tree"] = True
return self.macro_subpages(*args, **kwargs)
def macro_subpagetree(self, * args, **kwargs):
kwargs["tree"] = True
return self.macro_subpages(*args, **kwargs)
class page_list(list):
def __init__(self, request, *args, **kwargs):
self._request = request
return super().__init__(*args, **kwargs)
def sort_basename(self):
return list.sort(self, key=lambda x: os.path.basename(x.rel_path))
def creole_list(self, depth=None, filter_str='', parent_rel_path=''):
self.sort_basename()
depth = depth or 9999 # set a random high value if None
#
parent_rel_path = parent_rel_path + "/" if len(parent_rel_path) > 0 else ""
#
rv = ""
last_char = None
for page in self:
name = page.rel_path[len(parent_rel_path):]
if name.startswith(filter_str) and name != filter_str:
if name.count('/') < depth:
first_char = os.path.basename(name)[0].upper()
if last_char != first_char:
last_char = first_char
rv += f"=== {first_char}\n"
rv += f"* [[{url_page(page.rel_path)} | {name} ]]\n"
return rv
def html_list(self, depth=9999, filter_str='', parent_rel_path=''):
return mycreole.render_simple(self.creole_list(depth, filter_str, parent_rel_path))
class page_tree(dict):
T_PATTERN = "├── "
L_PATTERN = "└── "
I_PATTERN = ""
D_PATTERN = " "
def __init__(self, pl: page_list):
super().__init__()
for page in pl:
store_item = self
for entry in page.rel_path.split("/"):
if not entry in store_item:
store_item[entry] = {}
store_item = store_item[entry]
def html(self, rel_path=None, fill=""):
base = self
try:
for key in rel_path.split("/"):
base = base[key]
except AttributeError:
rel_path = ''
#
rv = ""
#
l = len(base)
for entry in sorted(list(base.keys())):
l -= 1
page_path = os.path.join(rel_path, entry)
page = page_wrapped(None, page_path)
if page.is_available():
entry = f'<a href="{url_page(page_path)}">{entry}</a>'
rv += fill + (self.L_PATTERN if l == 0 else self.T_PATTERN) + entry + "\n"
rv += self.html(page_path, fill=fill+(self.D_PATTERN if l == 0 else self.I_PATTERN))
return rv
class page_wrapped(object): class page_wrapped(object):
""" """
This class holds different page and meta instances and decides which will be used in which case. This class holds different page and meta instances and decides which will be used in which case.
@ -399,19 +207,9 @@ class page_wrapped(object):
self._request = request self._request = request
# #
page_path = self.__page_path__(path) page_path = self.__page_path__(path)
system_page_path = self.__system_page_path__(path)
# Page # Page
if request: self._page = page_data(page_path, history_version=history_version)
self._page = page_django(request, page_path, history_version=history_version)
else:
self._page = page_data(page_path, history_version=history_version)
self._page_meta = meta_data(page_path, history_version=history_version) self._page_meta = meta_data(page_path, history_version=history_version)
# System page
if request:
self._system_page = page_django(request, system_page_path)
else:
self._system_page = page_data(system_page_path)
self._system_meta_data = meta_data(system_page_path)
def __page_path__(self, path): def __page_path__(self, path):
if path.startswith(settings.PAGES_ROOT): if path.startswith(settings.PAGES_ROOT):
@ -421,20 +219,11 @@ class page_wrapped(object):
# must be a relative url # must be a relative url
return os.path.join(settings.PAGES_ROOT, path.replace("/", 2*SPLITCHAR)) return os.path.join(settings.PAGES_ROOT, path.replace("/", 2*SPLITCHAR))
def __system_page_path__(self, path):
return os.path.join(settings.SYSTEM_PAGES_ROOT, os.path.basename(path))
def __page_choose__(self): def __page_choose__(self):
if not self._page.is_available(): return self._page
return self._system_page
else:
return self._page
def __meta_choose__(self): def __meta_choose__(self):
if not self._page.is_available(): return self._page_meta
return self._system_meta_data
else:
return self._page_meta
def __store_history__(self): def __store_history__(self):
if self._page.is_available(): if self._page.is_available():
@ -454,6 +243,12 @@ class page_wrapped(object):
rv = meta.get(meta.KEY_CREATION_TIME) rv = meta.get(meta.KEY_CREATION_TIME)
return rv return rv
@property
def creation_user(self):
meta = self.__meta_choose__()
rv = meta.get(meta.KEY_CREATION_USER)
return rv
def delete(self): def delete(self):
self.__store_history__() self.__store_history__()
self._page.delete() self._page.delete()
@ -490,7 +285,7 @@ class page_wrapped(object):
return rv return rv
def is_available(self): def is_available(self):
return self._page.is_available() or self._system_page.is_available() return self._page.is_available()
def userpage_is_available(self): def userpage_is_available(self):
return self._page.is_available() return self._page.is_available()
@ -509,7 +304,7 @@ class page_wrapped(object):
def render_meta(self): def render_meta(self):
page = self.__page_choose__() page = self.__page_choose__()
rv = page.render_meta(self.creation_time, self.modified_time, self.modified_user, self.tags) rv = page.render_meta(self.creation_time, self.modified_time, self.creation_user, self.modified_user, self.tags)
return rv return rv
def render_to_html(self): def render_to_html(self):

View File

@ -8,7 +8,7 @@ from whoosh.fields import Schema, ID, TEXT, DATETIME
from whoosh.qparser.dateparse import DateParserPlugin from whoosh.qparser.dateparse import DateParserPlugin
from whoosh import index, qparser from whoosh import index, qparser
from pages.page import page_wrapped, full_path_all_pages from .models import PikiPage
logger = logging.getLogger(settings.ROOT_LOGGER_NAME).getChild(__name__) logger = logging.getLogger(settings.ROOT_LOGGER_NAME).getChild(__name__)
@ -38,11 +38,11 @@ def create_index():
def rebuild_index(ix): def rebuild_index(ix):
page_path = full_path_all_pages() pages = PikiPage.objects.all()
for path in page_path: for pp in pages:
pw = page_wrapped(None, path) if not pp.deleted:
add_item(ix, pw) add_item(ix, pp)
return len(page_path) return len(pages)
def load_index(): def load_index():
@ -56,19 +56,19 @@ def load_index():
return ix return ix
def add_item(ix, pw: page_wrapped): def add_item(ix, pp: PikiPage):
# Define Standard data # Define Standard data
# #
data = dict( data = dict(
id=pw.rel_path, id=pp.rel_path,
# #
title=pw.title, title=pp.title,
page_src=pw.raw_page_src, page_src=pp.page_txt,
tag=pw.tags, tag=pp.tags,
# #
creation_time=datetime.fromtimestamp(pw.creation_time), creation_time=pp.creation_time,
modified_time=datetime.fromtimestamp(pw.modified_time), modified_time=pp.modified_time,
modified_user=pw.modified_user modified_user=None if pp.modified_user is None else pp.modified_user.username
) )
with ix.writer() as w: with ix.writer() as w:
logger.info('Adding document with id=%s to the search index.', data.get('id')) logger.info('Adding document with id=%s to the search index.', data.get('id'))
@ -95,13 +95,13 @@ def whoosh_search(search_txt):
return rpl return rpl
def delete_item(ix, pw: page_wrapped): def delete_item(ix, pp: PikiPage):
with ix.writer() as w: with ix.writer() as w:
logger.info('Removing document with id=%s from the search index.', pw.rel_path) logger.info('Removing document with id=%s from the search index.', pp.rel_path)
w.delete_by_term("id", pw.rel_path) w.delete_by_term("id", pp.rel_path)
def update_item(pw: page_wrapped): def update_item(pp: PikiPage):
ix = load_index() ix = load_index()
delete_item(ix, pw) delete_item(ix, pp)
add_item(ix, pw) add_item(ix, pp)

View File

@ -6,7 +6,8 @@ from django.utils.translation import gettext as _
import logging import logging
from . import access
from .access import access_control
from . import messages from . import messages
from . import url_page from . import url_page
from . import get_search_query from . import get_search_query
@ -14,14 +15,25 @@ import config
from .context import context_adaption from .context import context_adaption
from .forms import EditForm, RenameForm from .forms import EditForm, RenameForm
from .help import help_pages from .help import help_pages
from .models import PikiPage, page_list
import mycreole import mycreole
from .page import page_wrapped, page_list from .search import whoosh_search, load_index, delete_item, add_item, update_item
from .search import whoosh_search, load_index, delete_item, add_item
from themes import Context from themes import Context
logger = logging.getLogger(settings.ROOT_LOGGER_NAME).getChild(__name__) logger = logging.getLogger(settings.ROOT_LOGGER_NAME).getChild(__name__)
SUCCESS_PAGE = _("""= Default startpage
**Congratulations!!!**
Seeing this page means, that you installed Piki successfull.
Edit this page to get your own first startpage.
If you need need assistance to edit a page, visit the [[/helpview/main|help pages]].
""")
def root(request): def root(request):
return HttpResponseRedirect(url_page(config.STARTPAGE)) return HttpResponseRedirect(url_page(config.STARTPAGE))
@ -29,19 +41,36 @@ def root(request):
def page(request, rel_path): def page(request, rel_path):
context = Context(request) # needs to be executed first because of time mesurement context = Context(request) # needs to be executed first because of time mesurement
# #
try:
p = PikiPage.objects.get(rel_path=rel_path)
except PikiPage.DoesNotExist:
p = None
meta = "meta" in request.GET meta = "meta" in request.GET
history = request.GET.get("history") history = request.GET.get("history")
if history: if history:
history = int(history) history = int(history)
# #
p = page_wrapped(request, rel_path, history_version=history) title = rel_path.split("/")[-1]
if access.read_page(request, rel_path): #
if meta: acc = access_control(request, rel_path)
page_content = p.render_meta() if acc.may_read():
if p is None or p.deleted:
if rel_path == config.STARTPAGE:
page_content = mycreole.render_simple(SUCCESS_PAGE)
else:
page_content = ""
if p is not None and p.deleted:
messages.deleted_page(request)
else:
messages.unavailable_msg_page(request, rel_path)
else: else:
page_content = p.render_to_html() title = p.title
if history: if meta:
messages.history_version_display(request, rel_path, history) page_content = p.render_meta(request, history)
else:
page_content = p.render_to_html(request, history)
if history:
messages.history_version_display(request, rel_path, history)
else: else:
messages.permission_denied_msg_page(request, rel_path) messages.permission_denied_msg_page(request, rel_path)
page_content = "" page_content = ""
@ -50,65 +79,75 @@ def page(request, rel_path):
context, context,
request, request,
rel_path=rel_path, rel_path=rel_path,
title=p.title, title=title,
upload_path=p.attachment_path, upload_path=rel_path,
page_content=page_content, page_content=page_content,
is_available=p.userpage_is_available() is_available=p is not None and not p.deleted
) )
return render(request, 'pages/page.html', context=context) return render(request, 'pages/page.html', context=context)
def edit(request, rel_path): def edit(request, rel_path):
if access.write_page(request, rel_path): acc = access_control(request, rel_path)
if acc.may_write():
context = Context(request) # needs to be executed first because of time mesurement context = Context(request) # needs to be executed first because of time mesurement
# #
try:
p = PikiPage.objects.get(rel_path=rel_path)
is_available = True
except PikiPage.DoesNotExist:
p = PikiPage(rel_path=rel_path)
is_available = False
#
if not request.POST: if not request.POST:
history = request.GET.get("history") history = request.GET.get("history")
if history: if history:
history = int(history) history = int(history)
# form = EditForm(instance=p.history.get(history_id=history))
p = page_wrapped(request, rel_path, history_version=history) else:
# form = EditForm(instance=p)
form = EditForm(page_data=p.raw_page_src, page_tags=p.tags)
# #
context_adaption( context_adaption(
context, context,
request, request,
rel_path=rel_path, rel_path=rel_path,
is_available=p.userpage_is_available(), is_available=is_available,
form=form, form=form,
# TODO: Add translation # TODO: Add translation
title=_("Edit page %s") % repr(p.title), title=_("Edit page %s") % repr(p.title),
upload_path=p.attachment_path, upload_path=rel_path,
) )
return render(request, 'pages/page_edit.html', context=context) return render(request, 'pages/page_edit.html', context=context)
else: else:
p = page_wrapped(request, rel_path) form = EditForm(request.POST, instance=p)
# #
save = request.POST.get("save") save = request.POST.get("save")
page_txt = request.POST.get("page_txt")
tags = request.POST.get("page_tags")
preview = request.POST.get("preview") preview = request.POST.get("preview")
# #
if save is not None: if save is not None:
if p.update_page(page_txt, tags): if form.is_valid():
messages.edit_success(request) form.instance.prepare_save(request)
page = form.save()
if page.save_needed:
messages.edit_success(request)
# update search index
update_item(page)
else:
messages.no_change(request)
else: else:
messages.no_change(request) messages.internal_error(request)
return HttpResponseRedirect(url_page(rel_path)) return HttpResponseRedirect(url_page(rel_path))
elif preview is not None: elif preview is not None:
form = EditForm(page_data=page_txt, page_tags=tags)
#
context_adaption( context_adaption(
context, context,
request, request,
rel_path=rel_path, rel_path=rel_path,
is_available=p.userpage_is_available(), is_available=is_available,
form=form, form=form,
# TODO: Add translation # TODO: Add translation
title=_("Edit page %s") % repr(p.title), title=_("Edit page %s") % repr(p.title),
upload_path=p.attachment_path, upload_path=rel_path,
page_content=p.render_text(request, page_txt) page_content=p.render_text(request, form.data.get("page_txt"))
) )
return render(request, 'pages/page_edit.html', context=context) return render(request, 'pages/page_edit.html', context=context)
else: else:
@ -119,11 +158,18 @@ def edit(request, rel_path):
def delete(request, rel_path): def delete(request, rel_path):
if access.write_page(request, rel_path): acc = access_control(request, rel_path)
if acc.may_write():
context = Context(request) # needs to be executed first because of time mesurement context = Context(request) # needs to be executed first because of time mesurement
# #
try:
p = PikiPage.objects.get(rel_path=rel_path)
is_available = True
except PikiPage.DoesNotExist:
p = PikiPage(rel_path=rel_path)
is_available = False
#
if not request.POST: if not request.POST:
p = page_wrapped(request, rel_path)
# #
# form = DeleteForm(page_data=p.raw_page_src, page_tags=p.tags) # form = DeleteForm(page_data=p.raw_page_src, page_tags=p.tags)
# #
@ -131,24 +177,21 @@ def delete(request, rel_path):
context, context,
request, request,
rel_path=rel_path, rel_path=rel_path,
is_available=p.userpage_is_available(), is_available=is_available,
# form=form,
# TODO: Add translation # TODO: Add translation
title=_("Delete page %s") % repr(p.title), title=_("Delete page %s") % repr(p.title),
upload_path=p.attachment_path, upload_path=rel_path,
page_content=p.render_to_html(), page_content=p.render_to_html(request),
) )
else: else:
p = page_wrapped(request, rel_path)
#
delete = request.POST.get("delete") delete = request.POST.get("delete")
# #
if delete: if delete:
p.deleted = True
p.save()
# delete page from search index # delete page from search index
ix = load_index() ix = load_index()
delete_item(ix, p) delete_item(ix, p)
# delete move files to history
p.delete()
# add delete message # add delete message
messages.page_deleted(request, p.title) messages.page_deleted(request, p.title)
return HttpResponseRedirect("/") return HttpResponseRedirect("/")
@ -162,28 +205,32 @@ def delete(request, rel_path):
def rename(request, rel_path): def rename(request, rel_path):
if access.write_page(request, rel_path): acc = access_control(request, rel_path)
if acc.may_write():
context = Context(request) # needs to be executed first because of time mesurement context = Context(request) # needs to be executed first because of time mesurement
# #
try:
p = PikiPage.objects.get(rel_path=rel_path)
is_available = True
except PikiPage.DoesNotExist:
p = PikiPage(rel_path=rel_path)
is_available = False
#
if not request.POST: if not request.POST:
p = page_wrapped(request, rel_path)
#
form = RenameForm(page_name=p.rel_path) form = RenameForm(page_name=p.rel_path)
# #
context_adaption( context_adaption(
context, context,
request, request,
rel_path=rel_path, rel_path=rel_path,
is_available=p.userpage_is_available(), is_available=is_available,
form=form, form=form,
# TODO: Add translation # TODO: Add translation
title=_("Delete page %s") % repr(p.title), title=_("Delete page %s") % repr(p.title),
upload_path=p.attachment_path, upload_path=rel_path,
page_content=p.render_to_html(), page_content=p.render_to_html(request),
) )
else: else:
p = page_wrapped(request, rel_path)
#
rename = request.POST.get("rename") rename = request.POST.get("rename")
page_name = request.POST.get("page_name") page_name = request.POST.get("page_name")
if rename: if rename:
@ -194,7 +241,8 @@ def rename(request, rel_path):
ix = load_index() ix = load_index()
delete_item(ix, p) delete_item(ix, p)
# rename the storage folder # rename the storage folder
p.rename(page_name) p.rel_path = page_name
p.save()
# add the renamed page to the search index # add the renamed page to the search index
add_item(ix, p) add_item(ix, p)
# add rename message # add rename message
@ -217,7 +265,7 @@ def search(request):
if sr is None: if sr is None:
django_messages.error(request, _('Invalid search pattern: %s') % repr(search_txt)) django_messages.error(request, _('Invalid search pattern: %s') % repr(search_txt))
sr = [] sr = []
pl = page_list(request, [page_wrapped(request, rel_path) for rel_path in set(sr)]) pl = page_list([PikiPage.objects.get(rel_path=rel_path) for rel_path in set(sr)])
# #
context_adaption( context_adaption(
context, context,

View File

@ -41,6 +41,8 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
#
'simple_history',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -124,7 +126,7 @@ STATIC_URL = 'static/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'data', 'media') MEDIA_ROOT = os.path.join(BASE_DIR, 'data', 'media')
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
MYCREOLE_ROOT = os.path.join(BASE_DIR, 'data', 'pages') MYCREOLE_ROOT = os.path.join(BASE_DIR, 'data', 'mycreole')
MYCREOLE_ATTACHMENT_ACCESS = { MYCREOLE_ATTACHMENT_ACCESS = {
'read': 'pages.access.read_attachment', 'read': 'pages.access.read_attachment',
'modify': 'pages.access.modify_attachment', 'modify': 'pages.access.modify_attachment',