diff --git a/tinywiki/migrations/0001_initial.py b/tinywiki/migrations/0001_initial.py index a13484c..1aaacbc 100644 --- a/tinywiki/migrations/0001_initial.py +++ b/tinywiki/migrations/0001_initial.py @@ -1,46 +1,86 @@ -# 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 - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Image', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('slug', models.SlugField(max_length=255, verbose_name='slug')), - ('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='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( - name='Page', - fields=[ - ('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(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')), - ], - ), - ] +# Generated by Django 5.2.9 on 2025-12-27 04:35 + +import django.db.models.deletion +import tinywiki.models +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='BuiltinImages', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('app', models.CharField(max_length=255, unique=True, verbose_name='app')), + ('version', models.PositiveIntegerField(verbose_name='version')), + ('prefix', models.CharField(verbose_name='prefix')), + ], + ), + migrations.CreateModel( + name='BuiltinPages', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('app', models.CharField(max_length=255, unique=True, verbose_name='app')), + ('version', models.PositiveIntegerField(verbose_name='version')), + ('prefix', models.CharField(verbose_name='prefix')), + ], + ), + migrations.CreateModel( + name='SidebarSection', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='title')), + ('priority', models.PositiveIntegerField(verbose_name='priority')), + ('is_visible', models.BooleanField(default=True, verbose_name='is visible')), + ], + ), + migrations.CreateModel( + name='Image', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.SlugField(max_length=255, verbose_name='slug')), + ('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='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( + name='Page', + fields=[ + ('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(choices=[('in_progress', 'in progress'), ('draft', 'draft'), ('published', 'published')], default='in_progress', 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(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')), + ], + ), + migrations.CreateModel( + name='SidebarEntry', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='Title')), + ('is_visible', models.BooleanField(default=True, verbose_name='is visible')), + ('priority', models.PositiveIntegerField(verbose_name='Priority')), + ('wiki_slug', models.CharField(blank=True, max_length=255, null=True, verbose_name='Wiki slug')), + ('url', models.CharField(blank=True, max_length=512, null=True, verbose_name='Link URL')), + ('widget', models.TextField(blank=True, null=True, verbose_name='Widget')), + ('section', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='tinywiki.sidebarsection', verbose_name='Section')), + ], + ), + ] diff --git a/tinywiki/migrations/0002_initial_data.py b/tinywiki/migrations/0002_initial_data.py index 4aad33c..88d4da4 100644 --- a/tinywiki/migrations/0002_initial_data.py +++ b/tinywiki/migrations/0002_initial_data.py @@ -1,132 +1,136 @@ -# Generated by Django 5.2.4 on 2025-09-14 13:15 - -from django.db import migrations -from .. import settings -class Migration(migrations.Migration): - dependencies = [ - ('tinywiki', '0001_initial'), - ] - - @staticmethod - def init_tinywiki_user(apps, schema_editor): - from django.contrib.auth import get_user_model - get_user_model().objects.create_user(**settings.TINYWIKI_USER_CONFIG) - - @staticmethod - 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_builtin_pages(apps,schema_editor)->None: - from ..models import Page,Image - 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_builtin_images(apps,schema_edit): - from pathlib import Path - from ..models import Image - from django.core.files.images import ImageFile - import json - - image_dir = Path(__file__).resolve().parent / "images" - images_json_file = image_dir/ "images.json" - - if not images_json_file.is_file(): - return - - with open(images_json_file,"rt",encoding="utf-8") as ifile: - images_data = json.loads(ifile.read()) - - for slug,_spec in images_data.items(): - spec = dict(_spec) - image_basename = spec['image'] - image_filename = image_dir / spec["image"] - spec['slug'] = slug - del spec['image'] - - img = Image(**spec) - with open(image_filename,"rb") as ifile: - img_file = ImageFile(ifile,image_basename) - img.image.save(image_basename,img_file) - img.save() - - - def init_user_imges(apps,schema_edit)->None: - #TODO - pass - - def init_user_pages(apps,schema_edit)->None: - #TODO - pass - - operations = [ - migrations.RunPython(init_tinywiki_groups_and_permissions), - migrations.RunPython(init_tinywiki_user), - migrations.RunPython(init_builtin_images), - migrations.RunPython(init_builtin_pages), - ] +# Generated by Django 5.2.4 on 2025-09-14 13:15 + +from django.db import migrations +from .. import settings + + +class Migration(migrations.Migration): + dependencies = [ + ('tinywiki', '0001_initial'), + ] + + @staticmethod + def init_tinywiki_user(apps, schema_editor): + from django.contrib.auth import get_user_model + get_user_model().objects.create_user(**settings.TINYWIKI_USER_CONFIG) + + @staticmethod + 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]) + + @staticmethod + def init_builtin_pages(apps, schema_editor): + from ..models import Page, BuiltinPages + 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()) + + version = data["version"] + app = data["app"] + prefix = data['prefix'] + + for slug, spec in data['pages'].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) + + BuiltinPages.objects.create(app=app, prefix=prefix, version=version) + + @staticmethod + def init_builtin_images(apps, schema_edit): + from pathlib import Path + from ..models import Image, BuiltinImages + from django.core.files.images import ImageFile + import json + + image_dir = Path(__file__).resolve().parent / "images" + images_json_file = image_dir / "images.json" + + if not images_json_file.is_file(): + return + + with open(images_json_file, "rt", encoding="utf-8") as ifile: + images_data = json.loads(ifile.read()) + + version = images_data['version'] + app = images_data['app'] + prefix = images_data['prefix'] + + for slug, _spec in images_data['images'].items(): + spec = dict(_spec) + image_basename = spec['image'] + image_filename = image_dir / spec["image"] + spec['slug'] = slug + del spec['image'] + + img = Image(**spec) + with open(image_filename,"rb") as ifile: + img_file = ImageFile(ifile,image_basename) + img.image.save(image_basename,img_file) + img.save() + + BuiltinImages.objects.create(app=app, prefix=prefix, version=version) + + + operations = [ + migrations.RunPython(init_tinywiki_groups_and_permissions), + migrations.RunPython(init_tinywiki_user), + migrations.RunPython(init_builtin_images), + migrations.RunPython(init_builtin_pages), + ] diff --git a/tinywiki/migrations/0004_default_sidebar.py b/tinywiki/migrations/0003_default_sidebar.py similarity index 97% rename from tinywiki/migrations/0004_default_sidebar.py rename to tinywiki/migrations/0003_default_sidebar.py index 3b1ccc7..b977bd0 100644 --- a/tinywiki/migrations/0004_default_sidebar.py +++ b/tinywiki/migrations/0003_default_sidebar.py @@ -85,7 +85,7 @@ class Migration(migrations.Migration): SidebarEntry.objects.create(section=section, **item_spec) dependencies = [ - ('tinywiki', '0003_sidebarsection_alter_page_status_data_sidebarentry'), + ('tinywiki', '0002_initial_data'), ] operations = [ diff --git a/tinywiki/migrations/0003_sidebarsection_alter_page_status_data_sidebarentry.py b/tinywiki/migrations/0003_sidebarsection_alter_page_status_data_sidebarentry.py deleted file mode 100644 index 4c37c14..0000000 --- a/tinywiki/migrations/0003_sidebarsection_alter_page_status_data_sidebarentry.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 5.2.9 on 2025-12-23 15:57 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('tinywiki', '0002_initial_data'), - ] - - operations = [ - migrations.CreateModel( - name='SidebarSection', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=255, verbose_name='title')), - ('priority', models.PositiveIntegerField(verbose_name='priority')), - ('is_visible', models.BooleanField(default=True, verbose_name='is visible')), - ], - ), - migrations.AlterField( - model_name='page', - name='status_data', - field=models.CharField(choices=[('in_progress', 'in progress'), ('draft', 'draft'), ('published', 'published')], default='in_progress', max_length=15, verbose_name='status'), - ), - migrations.CreateModel( - name='SidebarEntry', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=255, verbose_name='Title')), - ('is_visible', models.BooleanField(default=True, verbose_name='is visible')), - ('priority', models.PositiveIntegerField(verbose_name='Priority')), - ('wiki_slug', models.CharField(blank=True, max_length=255, null=True, verbose_name='Wiki slug')), - ('url', models.CharField(blank=True, max_length=512, null=True, verbose_name='Link URL')), - ('widget', models.TextField(blank=True, null=True, verbose_name='Widget')), - ('section', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='tinywiki.sidebarsection', verbose_name='Section')), - ], - ), - ] diff --git a/tinywiki/migrations/images/images.json b/tinywiki/migrations/images/images.json index 90a17fb..2927c0b 100644 --- a/tinywiki/migrations/images/images.json +++ b/tinywiki/migrations/images/images.json @@ -1,7 +1,12 @@ { - "tw-image-01": { - "alt": "Foggy Mountain-tops in the dawn", - "image": "fabian-bachli-jQAe44MEIXU-unsplash.jpg", - "description": "Foto from [url=\"https://unsplash.com/de/@fabianbaechli?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash\"]Fabian Bächli[/url] on [url=\"https://unsplash.com/de/fotos/nebelige-berggipfel-in-der-morgendammerung-mit-sanften-wolken-jQAe44MEIXU?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash\"]Unsplash[/url]" + "version": 0, + "app": "tinywiki", + "prefix": "tw-", + "images": { + "tw-image-01": { + "alt": "Foggy Mountain-tops in the dawn", + "image": "fabian-bachli-jQAe44MEIXU-unsplash.jpg", + "description": "Foto from [url=\"https://unsplash.com/de/@fabianbaechli?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash\"]Fabian Bächli[/url] on [url=\"https://unsplash.com/de/fotos/nebelige-berggipfel-in-der-morgendammerung-mit-sanften-wolken-jQAe44MEIXU?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash\"]Unsplash[/url]" + } } } \ No newline at end of file diff --git a/tinywiki/migrations/pages/pages.json b/tinywiki/migrations/pages/pages.json index 27f40a5..4718aa6 100644 --- a/tinywiki/migrations/pages/pages.json +++ b/tinywiki/migrations/pages/pages.json @@ -1,26 +1,31 @@ { - "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" - }, - "tw-bbcode": { - "title": "BBCode used by TinyWiki", - "content_type": "bbcode", - "status": "published", - "file": "bbcode.bbcode" - }, - "tw-markdown": { - "title": "Markdown used by TinyWiki", - "content_type": "markdown", - "status": "draft", - "file": "markdown.md" + "version": 0, + "app": "tinywiki", + "prefix": "tw-", + "pages": { + "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" + }, + "tw-bbcode": { + "title": "BBCode used by TinyWiki", + "content_type": "bbcode", + "status": "published", + "file": "bbcode.bbcode" + }, + "tw-markdown": { + "title": "Markdown used by TinyWiki", + "content_type": "markdown", + "status": "draft", + "file": "markdown.md" + } } } \ No newline at end of file diff --git a/tinywiki/models.py b/tinywiki/models.py index 3cc20d6..ca163f1 100644 --- a/tinywiki/models.py +++ b/tinywiki/models.py @@ -60,7 +60,6 @@ class Page(models.Model): null=False, blank=False) - created_at = models.DateTimeField(_("created at"), auto_now_add=True) created_by = models.ForeignKey(get_user_model(), @@ -80,6 +79,7 @@ class Page(models.Model): @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): @@ -92,6 +92,7 @@ class Page(models.Model): @property def status(self)->WikiPageStatus: return WikiPageStatus.from_string(self.status_data) + @status.setter def status(self, status: str | WikiPageStatus): if isinstance(status, str): @@ -215,3 +216,19 @@ class SidebarEntry(models.Model): return mark_safe(f"
  • {self.widget}
  • ") else: return mark_safe(f"
  • {self.link}
  • ") + + +class BuiltinPages(models.Model): + app = models.CharField(_("app"), + max_length=255, + unique=True) + version = models.PositiveIntegerField(_("version")) + prefix = models.CharField(_("prefix")) + + +class BuiltinImages(models.Model): + app = models.CharField(_("app"), + max_length=255, + unique=True) + version = models.PositiveIntegerField(_("version")) + prefix = models.CharField(_("prefix")) diff --git a/tinywiki/utils.py b/tinywiki/utils.py new file mode 100644 index 0000000..6314db9 --- /dev/null +++ b/tinywiki/utils.py @@ -0,0 +1,367 @@ +from pathlib import Path +import json +import zipfile +import os +import sys + +from .models import Image, Page, BuiltinImages, BuiltinPages, get_tinywiki_default_user +from .enums import WikiContentType, WikiPageStatus +from django.core.files.images import ImageFile +from django.conf import settings as django_settings + + +def import_builtin_pages(json_file: str | Path, user=None): + """ + Import builtin pages + + :param json_file: The json file which holds the data. + :type json_file: str | Path + :raises ValueError: If file does not exist or json_file is not a file. + :return: `True` if pages were successfully imported, `False` otherwise + :rvalue: bool + """ + if isinstance(json_file, str): + json_file = Path(json_file).resolve() + + if not json_file.exists(): + raise ValueError("File \"{json_file}\" does not exist!") + if not json_file.isfile(): + raise ValueError("json_file needs to be a valid file!") + + with open(json_file, "rt", encoding="utf-8") as ifile: + data = json.loads(ifile.read()) + + app = data['app'] + prefix = data['prefix'] + version = data['version'] + + page_path = json_file.parent + + try: + bp = BuiltinPages.objects.get(app=app) + if version <= bp.version: + print("Page already imported, skipping!") + return False + + for slug, spec in data['pages'].items(): + filename = page_path / spec['file'] + with open(filename, "rt", encoding="utf-8") as ifile: + content = ifile.read() + + try: + p = Page.objects.get() + p.title = spec['title'] + p.status_data = WikiPageStatus.from_string(spec['status']).value + p.content_type_data = WikiContentType.from_string(spec['content_type']).value, + p.content = content + p.author = user if user else get_tinywiki_default_user() + p.save() + except Page.DoesNotExist: + 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, + author=user if user else get_tinywiki_default_user()) + bp.prefix = prefix + bp.version = version + bp.save() + return True + except BuiltinPages.DoesNotExist: + for slug, spec in data['pages'].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, + author=user if user else get_tinywiki_default_user()) + BuiltinPages.objects.create(app=app, prefix=prefix, version=version) + return True + + +def import_builtin_pages_from_zip(zip: str | Path, user=None): + """ + Import builtin pages + + :param zip: The zip file which holds the data. + :type zip: str | Path + :raises ValueError: If file does not exist or json_file is not a file. + :return: `True` if pages were successfully imported, `False` otherwise + :rvalue: bool + """ + if isinstance(zip, str): + zip = Path(zip).resolve() + + if not zip.exists(): + raise ValueError(f"File \"{zip}\" does not exist!") + if not zip.isfile(): + raise ValueError("zip needs to be a valid file!") + if not zipfile.is_zipfile(zip): + raise ValueError("zip needs to be a zip file!") + + + with zipfile.ZipFile(zip, "r") as zf: + with zf.open('pages.json') as json_file: + data = json.loads(json_file.read().decode('utf-8')) + + app = data['app'] + prefix = data['prefix'] + version = data['version'] + + try: + bp = BuiltinPages.objects.get(app=app) + if version <= bp.version: + print("Page already imported, skipping!") + return False + + for slug, spec in data['pages'].items(): + with zf.open(spec['file']) as ifile: + content = ifile.read().decode('utf-8') + + try: + p = Page.objects.get() + p.title = spec['title'] + p.status_data = WikiPageStatus.from_string(spec['status']).value + p.content_type_data = WikiContentType.from_string(spec['content_type']).value, + p.content = content + p.author = user if user else get_tinywiki_default_user() + p.save() + except Page.DoesNotExist: + 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, + author=user if user else get_tinywiki_default_user()) + bp.prefix = prefix + bp.version = version + bp.save() + return True + except BuiltinPages.DoesNotExist: + for slug, spec in data['pages'].items(): + with zf.open(spec['file'], "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, + author=user if user else get_tinywiki_default_user()) + BuiltinPages.objects.create(app=app, prefix=prefix, version=version) + return True + + +def import_builtin_images(json_file: str | Path, user=None): + if isinstance(json_file, str): + json_file = Path(json_file).resolve() + if not json_file.exists(): + raise ValueError(f"File \"{json_file}\" does not exist!") + if not json_file.isfile(): + raise ValueError("json_file needs to be a valid file!") + + image_dir = json_file.parent + + with open(json_file, "rt", encoding="utf-8") as ifile: + data = json.loads(ifile.read()) + + version = data['version'] + app = data['app'] + prefix = data['prefix'] + + try: + bi = BuiltinImages.objects.get(app=app) + if bi.version <= data['version']: + return False + + for slug, spec in data['images'].items(): + try: + img = Image.objects.get(slug=spec['slug']) + continue + except Image.DoesNotExist: + image_basename = spec['image'] + image_filename = image_dir / spec.pop("image") + spec['slug'] = slug + spec['user'] = user if user else get_tinywiki_default_user() + + img = Image(**spec) + with open(image_filename, "rb") as ifile: + img_file = ImageFile(ifile, image_basename) + img.image.save(image_basename, img_file) + img.save() + bi.version = version + bi.save() + return True + except BuiltinImages.DoesNotExist: + for slug, spec in data['images'].items(): + spec = spec + image_basename = spec['image'] + image_filename = image_dir / spec.pop("image") + spec['slug'] = slug + spec['user'] = user if user else get_tinywiki_default_user() + + img = Image(**spec) + with open(image_filename, "rb") as ifile: + img_file = ImageFile(ifile, image_basename) + img.image.save(image_basename, img_file) + img.save() + + BuiltinImages.objects.create(app=app, prefix=prefix, version=version) + return True + + +def import_builtin_images_from_zip(zip: str | Path, user=None): + if isinstance(zip, str): + zip = Path(zip).resolve() + if not zip.exists(): + raise ValueError(f"File \"{zip}\" does not exist!") + if not zip.isfile(): + raise ValueError("zip needs to be a valid file!") + if not zipfile.is_zipfile(zip): + raise ValueError("zip needs to be a zip file!") + + with zipfile.ZipFile(zip, "r", encoding="utf-8") as zf: + data = json.loads(zf.open('images.json').read().decode('utf-8')) + + version = data['version'] + app = data['app'] + prefix = data['prefix'] + + try: + bi = BuiltinImages.objects.get(app=app) + if bi.version <= data['version']: + return False + + for slug, spec in data['images'].items(): + try: + img = Image.objects.get(slug=spec['slug']) + continue + except Image.DoesNotExist: + image_basename = os.path.basename(spec['image']) + image_filename = spec.pop("image") + spec['slug'] = slug + spec['user'] = user if user else get_tinywiki_default_user() + + img = Image(**spec) + with zf.open(image_filename) as ifile: + img_file = ImageFile(ifile, image_basename) + img.image.save(image_basename, img_file) + img.save() + bi.version = version + bi.save() + return True + except BuiltinImages.DoesNotExist: + for slug, spec in data['images'].items(): + spec = spec + image_basename = os.path.basename(spec['image']) + image_filename = spec.pop("image") + spec['slug'] = slug + spec['user'] = user if user else get_tinywiki_default_user() + + img = Imagwith zie(**spec) + with zf.open(image_filename) as ifile: + img_file = ImageFile(ifile, image_basename) + img.image.save(image_basename, img_file) + img.save() + + BuiltinImages.objects.create(app=app, prefix=prefix, version=version) + return True + +def export_wiki_content(app: str, + filename, + prefix: str|None = None, + page_version: int = 0, + image_version: int = 0) -> bool: + prefix = prefix + try: + bp = BuiltinPages.objects.get(app=app) + if page_version < bp.version: + page_version = bp.version + 1 + if prefix is None: + prefix = bp.prefix + except BuiltinPages.DoesNotExist: + bp = None + + try: + bi = BuiltinImages.objects.get(app=app) + if image_version < bi.version: + image_version = bi.version + 1 + if prefix is None: + prefix = bp.prefix + except BuiltinImages.DoesNotExist: + bi = None + + if not prefix: + raise RuntimeError("No slug prefix! Can not export!", file=sys.stderr) + + pages = Page.objects.filter(slug__startswith=prefix) + images = Image.objects.filter(slug__statrswith=prefix) + + if not pages and not images: + return False + + with zipfile.ZipFile(filename, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf: + if pages: + + pages_data = { + 'app': app, + 'prefix': prefix, + 'version': page_version, + 'pages': {} + } + + for p in pages: + if WikiContentType.BBCODE == p.content_type: + extension = 'bbcode' + else: + extension = 'md' + page_file = f"pages/{p.slug}.{extension}" + pages_data['pages'][p.slug] = { + 'title': p.title, + 'content_type': p.content_type.value, + 'status': p.status.value, + 'file': page_file, + } + zf.writestr(page_file, p.content) + zf.writestr('pages.json', json.dumps(pages_data, ensure_ascii=False, indent=4)) + if bp: + bp.version = page_version + bp.save() + else: + BuiltinPages.objects.create(app=app, prefix=prefix, version=page_version) + + if images: + images_data = { + 'app': app, + 'prefix': prefix, + 'version': image_version, + 'images': {} + } + for i in images: + if not os.path.isfile(i.image.path): + continue + + arcname = f"images/{os.path.basename(i.image.path)}" + images_data['images'][i.slug] = { + 'slug': i.slug, + 'alt': i.alt, + 'file': arcname, + 'description': i.description, + } + zf.write(i.image.path, arcname) + zf.writestr(images.json, json.dumps(images_data, ensure_ascii=False, indent=4)) + if bi: + bi.version = image_version + bi.save() + else: + BuiltinImages.objects.create(app=app, prefix=prefix, version=image_version) + return True + +def export_tinywiki_wiki_content(filename=None): + if filename is None: + filename = Path(django_settings.MEDIA_ROOT) / "tinywiki-tinywiki.zip" + export_wiki_content('tinywiki', filename, 'tw-')