Browse Source

Export theme, WIP import theme

rafalp 6 years ago
parent
commit
501b88d8bc

+ 1 - 1
misago/templates/misago/admin/agreements/form.html

@@ -33,7 +33,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 1 - 1
misago/templates/misago/admin/attachmenttypes/form.html

@@ -33,7 +33,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 1 - 1
misago/templates/misago/admin/bans/form.html

@@ -33,7 +33,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 1 - 1
misago/templates/misago/admin/categories/form.html

@@ -33,7 +33,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 1 - 1
misago/templates/misago/admin/categoryroles/form.html

@@ -33,7 +33,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 1 - 1
misago/templates/misago/admin/datadownloads/form.html

@@ -19,7 +19,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 1 - 1
misago/templates/misago/admin/generic/form.html

@@ -16,7 +16,7 @@
   <div class="col-xs-12 {% block form-main-col-class %}col-md-8 col-md-offset-2{% endblock form-main-col-class %}">
 
     <div class="form-panel">
-      <form role="form" method="post" {% block form-extra %}{% endblock form-extra%}>
+      <form role="form" method="post" {% block form-extra %}{% endblock form-extra %}>
         {% csrf_token %}
 
         <div class="form-header">

+ 1 - 1
misago/templates/misago/admin/ranks/form.html

@@ -33,7 +33,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 1 - 1
misago/templates/misago/admin/roles/form.html

@@ -33,7 +33,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 1 - 1
misago/templates/misago/admin/themes/assets/css-editor-form.html

@@ -46,7 +46,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 1 - 1
misago/templates/misago/admin/themes/assets/css-link-form.html

@@ -31,7 +31,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 1 - 1
misago/templates/misago/admin/themes/form.html

@@ -33,7 +33,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 43 - 0
misago/templates/misago/admin/themes/import.html

@@ -0,0 +1,43 @@
+{% extends "misago/admin/generic/form.html" %}
+{% load i18n misago_admin_form %}
+
+
+{% block title %}
+{% trans "Import theme" %} | {{ active_link.name }} | {{ block.super }}
+{% endblock title %}
+
+
+{% block page-target %}
+{% trans "Import theme" %}
+{% endblock page-target %}
+
+
+{% block form-header %}
+<h1>
+  {% trans "Import theme" %}
+</h1>
+{% endblock %}
+
+
+{% block form-extra %}
+class="form-horizontal" enctype="multipart/form-data"
+{% endblock form-extra %}
+
+{% block form-body %}
+<div class="form-body no-fieldsets">
+  {% with label_class="col-md-3" field_class="col-md-9" %}
+    {% form_row form.name label_class field_class %}
+    {% form_row form.upload label_class field_class %}
+  {% endwith %}
+</div>
+{% endblock form-body %}
+
+
+{% block form-footer-class %}
+col-md-offset-3
+{% endblock form-footer-class %}
+
+
+{% block form-footer %}
+<button class="btn btn-primary">{% trans "Import" %}</button>
+{% endblock %}

+ 25 - 6
misago/templates/misago/admin/themes/list.html

@@ -4,9 +4,13 @@
 
 {% block page-actions %}
 <div class="page-actions">
+    <a href="{% url 'misago:admin:appearance:themes:import' %}" class="btn btn-success">
+      <span class="fa fa-upload"></span>
+      {% trans "Import theme" %}
+    </a>
   <a href="{% url 'misago:admin:appearance:themes:new' %}" class="btn btn-success">
     <span class="fa fa-plus-circle"></span>
-    {% trans "New theme" %}
+    {% trans "Create theme" %}
   </a>
 </div>
 {% endblock %}
@@ -20,6 +24,7 @@
 <th>&nbsp;</th>
 <th>&nbsp;</th>
 <th>&nbsp;</th>
+<th>&nbsp;</th>
 {% endblock table-header %}
 
 
@@ -46,7 +51,7 @@
 </td>
 <td class="row-action">
   {% if not item.is_default %}
-    <a href="{% url 'misago:admin:appearance:themes:assets' pk=item.pk %}" class="btn btn-primary tooltip-top" title="{% trans "Assets (CSS, images and fonts)" %}">
+    <a href="{% url 'misago:admin:appearance:themes:assets' pk=item.pk %}" class="btn btn-primary tooltip-top" title="{% trans 'Assets (CSS, images and fonts)' %}">
       <span class="fa fa-pencil-square-o"></span>
     </a>
   {% else %}
@@ -57,7 +62,7 @@
 </td>
 <td class="row-action">
   {% if not item.is_default %}
-    <a href="{% url 'misago:admin:appearance:themes:edit' pk=item.pk %}" class="btn btn-primary tooltip-top" title="{% trans "Edit information" %}">
+    <a href="{% url 'misago:admin:appearance:themes:edit' pk=item.pk %}" class="btn btn-primary tooltip-top" title="{% trans 'Edit information' %}">
       <span class="fa fa-pencil"></span>
     </a>
   {% else %}
@@ -67,14 +72,14 @@
   {% endif %}
 </td>
 <td class="row-action">
-  <a href="{% url 'misago:admin:appearance:themes:new' %}?parent={{ item.pk }}" class="btn btn-primary tooltip-top" title="{% trans "Create child theme" %}">
+  <a href="{% url 'misago:admin:appearance:themes:new' %}?parent={{ item.pk }}" class="btn btn-primary tooltip-top" title="{% trans 'Create child theme' %}">
     <span class="fa fa-file-o"></span>
   </a>
 </td>
 <td class="row-action">
   {% if not item.is_active %}
     <form action="{% url 'misago:admin:appearance:themes:activate' pk=item.pk %}" method="post">
-      <button class="btn btn-default tooltip-top" title="{% trans "Activate" %}">
+      <button class="btn btn-default tooltip-top" title="{% trans 'Activate' %}">
         {% csrf_token %}
         <span class="fa fa-check-square"></span>
       </button>
@@ -86,10 +91,24 @@
   {% endif %}
 </td>
 <td class="row-action">
+  {% if not item.is_default %}
+    <form action="{% url 'misago:admin:appearance:themes:export' pk=item.pk %}" method="POST">
+      {% csrf_token %}
+      <button class="btn btn-primary tooltip-top" title="{% trans 'Export' %}">
+        <span class="fa fa-download"></span>
+      </button>
+    </form>
+  {% else %}
+    <button class="btn" type="button" disabled>
+      <span class="fa fa-download"></span>
+    </button>
+  {% endif %}
+</td>
+<td class="row-action">
   {% if not item.is_active and not item.is_default %}
     <form action="{% url 'misago:admin:appearance:themes:delete' pk=item.pk %}" method="POST" class="delete-prompt">
       {% csrf_token %}
-      <button class="btn btn-danger tooltip-top" title="{% trans "Delete" %}">
+      <button class="btn btn-danger tooltip-top" title="{% trans 'Delete' %}">
         <span class="fa fa-times"></span>
       </button>
     </form>

+ 1 - 1
misago/templates/misago/admin/users/ban.html

@@ -19,7 +19,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 1 - 1
misago/templates/misago/admin/users/edit.html

@@ -21,7 +21,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 1 - 1
misago/templates/misago/admin/users/new.html

@@ -19,7 +19,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 1 - 1
misago/templates/misago/admin/warnings/form.html

@@ -33,7 +33,7 @@
 
 {% block form-extra %}
 class="form-horizontal"
-{% endblock form-extra%}
+{% endblock form-extra %}
 
 
 {% block form-body %}

+ 4 - 0
misago/themes/admin/__init__.py

@@ -14,6 +14,8 @@ from .views import (
     NewTheme,
     NewThemeCss,
     NewThemeCssLink,
+    ExportTheme,
+    ImportTheme,
     ThemeAssets,
     ThemesList,
     UploadThemeCss,
@@ -35,6 +37,8 @@ class MisagoAdminExtension:
             url(r"^edit/(?P<pk>\d+)/$", EditTheme.as_view(), name="edit"),
             url(r"^delete/(?P<pk>\d+)/$", DeleteTheme.as_view(), name="delete"),
             url(r"^activate/(?P<pk>\d+)/$", ActivateTheme.as_view(), name="activate"),
+            url(r"^export/(?P<pk>\d+)/$", ExportTheme.as_view(), name="export"),
+            url(r"^import/$", ImportTheme.as_view(), name="import"),
             url(r"^assets/(?P<pk>\d+)/$", ThemeAssets.as_view(), name="assets"),
             url(
                 r"^assets/(?P<pk>\d+)/delete-css/$",

+ 113 - 0
misago/themes/admin/exporter.py

@@ -0,0 +1,113 @@
+import json
+import os
+import shutil
+from tempfile import TemporaryDirectory
+
+from django.http import FileResponse
+
+from ...core.utils import slugify
+
+
+def export_theme(theme):
+    with TemporaryDirectory() as tmp_dir:
+        export_dir = create_export_directory(tmp_dir, theme)
+
+        manifest = create_theme_manifest(theme)
+
+        manifest["css"] = write_theme_css(export_dir, theme)
+        manifest["media"] = write_theme_media(export_dir, theme)
+        write_theme_manifest(export_dir, manifest)
+
+        export_file = zip_theme_export(tmp_dir, export_dir)
+        export_filename = os.path.split(export_file)[-1]
+
+        response = FileResponse(open(export_file, "rb"), content_type="application/zip")
+        response['Content-Length'] = os.path.getsize(export_file)
+        response['Content-Disposition'] = "inline; filename=%s" % export_filename
+
+        return response
+
+
+def create_export_directory(tmp_dir, theme):
+    export_name = get_export_name(theme)
+    export_dir = os.path.join(tmp_dir, export_name)
+    os.mkdir(export_dir)
+    return export_dir
+
+
+def get_export_name(theme):
+    if theme.version:
+        return "%s-%s" % (slugify(theme.name), theme.version.replace(".", "-"))
+    return slugify(theme.name)
+
+
+def create_theme_manifest(theme):
+    return {
+        "name": theme.name,
+        "version": theme.version,
+        "author": theme.author,
+        "url": theme.url,
+        "css": [],
+        "media": [],
+    }
+
+
+def write_theme_css(export_dir, theme):
+    files_dir = create_sub_directory(export_dir, "css")
+    files = []
+
+    for css in theme.css.all():
+        if css.url:
+            files.append({
+                "name": css.name,
+                "url": css.url,
+            })
+        else:
+            files.append({
+                "name": css.name,
+                "path": copy_asset_file(files_dir, css.source_file),
+            })
+
+    return files
+
+
+def write_theme_media(export_dir, theme):
+    files_dir = create_sub_directory(export_dir, "media")
+    files = []
+
+    for media in theme.media.all():
+        files.append({
+            "name": media.name,
+            "type": media.type,
+            "path": copy_asset_file(files_dir, media.file),
+        })
+
+    return files
+
+
+def create_sub_directory(export_dir, dirname):
+    new_dir = os.path.join(export_dir, dirname)
+    os.mkdir(new_dir)
+    return new_dir
+
+
+def copy_asset_file(export_dir, asset_file):
+    filename = os.path.split(asset_file.name)[-1]
+    dst_path = os.path.join(export_dir, filename)
+    with open(dst_path, "wb") as fp:
+        for chunk in asset_file.chunks():
+            fp.write(chunk)
+    dirname = os.path.basename(export_dir)
+    return os.path.join(dirname, filename)
+
+
+def write_theme_manifest(export_dir, manifest):
+    manifest_path = os.path.join(export_dir, "manifest.json")
+    with open(manifest_path, "w") as fp:
+        json.dump(manifest, fp, ensure_ascii=False, indent=2)
+
+
+def zip_theme_export(tmp_dir, export_dir):
+    dir_name = os.path.split(export_dir)[-1]
+    zip_name = shutil.make_archive(dir_name, 'zip', tmp_dir)
+    return os.path.join(tmp_dir, zip_name)

+ 22 - 0
misago/themes/admin/forms.py

@@ -46,6 +46,28 @@ class ThemeForm(forms.ModelForm):
         )
 
 
+class ImportForm(forms.Form):
+    name = forms.CharField(
+        label=_("Name"),
+        help_text=_("Leave this field empty to use theme name from imported file."),
+        max_length=255,
+        required=False,
+    )
+    upload = forms.FileField(
+        label=_("Theme file"),
+        help_text=_("Theme file should be a zip file."),
+    )
+
+    def clean_upload(self):
+        data = self.cleaned_data["upload"]
+        error_message = gettext("Uploaded file is not a valid Zip file.")
+        if not data.name.lower().endswith(".zip"):
+            raise forms.ValidationError(error_message)
+        if data.content_type not in ("application/zip", "application/octet-stream"):
+            raise forms.ValidationError(error_message)
+        return data
+
+
 class UploadAssetsForm(forms.Form):
     allowed_content_types = []
     allowed_extensions = []

+ 13 - 0
misago/themes/admin/importer.py

@@ -0,0 +1,13 @@
+from tempfile import TemporaryDirectory
+
+from django.utils.translation import gettext as _
+
+
+class ThemeImportError(BaseException):
+    pass
+
+
+def import_theme(name, zipfile):
+    with TemporaryDirectory() as tmp_dir:
+        pass
+    

+ 26 - 1
misago/themes/admin/views.py

@@ -7,7 +7,9 @@ from ...admin.views import generic
 from ..cache import clear_theme_cache
 from ..models import Theme, Css
 from .css import move_css_down, move_css_up
-from .forms import CssEditorForm, CssLinkForm, ThemeForm, UploadCssForm, UploadMediaForm
+from .exporter import export_theme
+from .forms import CssEditorForm, CssLinkForm, ImportForm, ThemeForm, UploadCssForm, UploadMediaForm
+from .importer import ThemeImportError, import_theme
 from .tasks import build_single_theme_css, build_theme_css, update_remote_css_size
 
 
@@ -88,6 +90,29 @@ def set_theme_as_active(request, theme):
     clear_theme_cache()
 
 
+class ExportTheme(ThemeAdmin, generic.ButtonView):
+    def button_action(self, request, target):
+        return export_theme(target)
+
+
+class ImportTheme(ThemeAdmin, generic.FormView):
+    form = ImportForm
+    template = "import.html"
+
+    def handle_form(self, form, request):
+        try:
+            self.import_theme(request, **form.cleaned_data)
+            return redirect(self.root_link)
+        except ThemeImportError as e:
+            form.add_error("upload", str(e))
+            return self.render(request, {"form": form})
+
+    def import_theme(self, request, *_, name, upload):
+        theme = import_theme(name, upload)
+        message = gettext('Theme "%(name)s" has been imported.')
+        messages.success(request, message % {"name": theme})
+
+
 class ThemeAssetsAdmin(ThemeAdmin):
     def check_permissions(self, request, theme):
         if theme.is_default: