Browse Source

Redo theme images into media

rafalp 6 years ago
parent
commit
d663983cc5

+ 8 - 14
misago/admin/admin.py

@@ -6,14 +6,13 @@ from .themes.views import (
     ActivateTheme,
     ActivateTheme,
     DeleteTheme,
     DeleteTheme,
     DeleteThemeCss,
     DeleteThemeCss,
-    DeleteThemeFonts,
-    DeleteThemeImages,
+    DeleteThemeMedia,
     EditTheme,
     EditTheme,
     NewTheme,
     NewTheme,
     ThemeAssets,
     ThemeAssets,
     ThemesList,
     ThemesList,
     UploadThemeCss,
     UploadThemeCss,
-    UploadThemeImages,
+    UploadThemeMedia,
 )
 )
 
 
 
 
@@ -38,14 +37,9 @@ class MisagoAdminExtension(MiddlewareMixin):
                 name="delete-css",
                 name="delete-css",
             ),
             ),
             url(
             url(
-                r"^assets/(?P<pk>\d+)/delete-fonts/$",
-                DeleteThemeFonts.as_view(),
-                name="delete-fonts",
-            ),
-            url(
-                r"^assets/(?P<pk>\d+)/delete-images/$",
-                DeleteThemeImages.as_view(),
-                name="delete-images",
+                r"^assets/(?P<pk>\d+)/delete-media/$",
+                DeleteThemeMedia.as_view(),
+                name="delete-media",
             ),
             ),
             url(
             url(
                 r"^assets/(?P<pk>\d+)/upload-css/$",
                 r"^assets/(?P<pk>\d+)/upload-css/$",
@@ -53,9 +47,9 @@ class MisagoAdminExtension(MiddlewareMixin):
                 name="upload-css",
                 name="upload-css",
             ),
             ),
             url(
             url(
-                r"^assets/(?P<pk>\d+)/upload-images/$",
-                UploadThemeImages.as_view(),
-                name="upload-images",
+                r"^assets/(?P<pk>\d+)/upload-media/$",
+                UploadThemeMedia.as_view(),
+                name="upload-media",
             ),
             ),
         )
         )
 
 

+ 0 - 72
misago/admin/themes/assets.py

@@ -1,72 +0,0 @@
-import hashlib
-import io
-
-from PIL import Image
-from django.core.files.images import ImageFile
-
-IMAGE_THUMBNAIL_SIZE = (32, 32)
-
-
-def create_css(theme, css):
-    if css_exists(theme, css):
-        delete_css(theme, css)
-    save_css(theme, css)
-
-
-def css_exists(theme, css):
-    return theme.css.filter(name=css.name).exists()
-
-
-def delete_css(theme, css):
-    theme.css.get(name=css.name).delete()
-
-
-def save_css(theme, css):
-    theme.css.create(
-        name=css.name, source_file=css, source_hash=get_file_hash(css), size=css.size
-    )
-
-
-def create_image(theme, image):
-    if image_exists(theme, image):
-        delete_image(theme, image)
-    save_image(theme, image)
-
-
-def image_exists(theme, image):
-    return theme.images.filter(name=image.name).exists()
-
-
-def delete_image(theme, image):
-    theme.images.get(name=image.name).delete()
-
-
-def save_image(theme, image):
-    theme.images.create(
-        name=image.name,
-        file=image,
-        hash=get_file_hash(image),
-        type=image.content_type,
-        size=image.size,
-        thumbnail=get_image_thumbnail(image),
-    )
-
-
-def get_image_thumbnail(image):
-    img = Image.open(image.file)
-    img.thumbnail(IMAGE_THUMBNAIL_SIZE)
-    file = io.BytesIO()
-    img.save(file, format="png")
-    img.close()
-
-    filename = image.name.split(".")[0]
-    return ImageFile(file, name="thumb_%s.png" % filename)
-
-
-def get_file_hash(file):
-    if file.size is None:
-        return "00000000"
-    file_hash = hashlib.md5()
-    for chunk in file.chunks():
-        file_hash.update(chunk)
-    return file_hash.hexdigest()[:8]

+ 21 - 0
misago/admin/themes/css.py

@@ -0,0 +1,21 @@
+from .utils import get_file_hash
+
+
+def create_css(theme, css):
+    if css_exists(theme, css):
+        delete_css(theme, css)
+    save_css(theme, css)
+
+
+def css_exists(theme, css):
+    return theme.css.filter(name=css.name).exists()
+
+
+def delete_css(theme, css):
+    theme.css.get(name=css.name).delete()
+
+
+def save_css(theme, css):
+    theme.css.create(
+        name=css.name, source_file=css, source_hash=get_file_hash(css), size=css.size
+    )

+ 4 - 5
misago/admin/themes/forms.py

@@ -5,7 +5,8 @@ from mptt.forms import TreeNodeChoiceField
 
 
 from ...themes.models import Theme
 from ...themes.models import Theme
 from ..forms import YesNoSwitch
 from ..forms import YesNoSwitch
-from .assets import create_css, create_image
+from .css import create_css
+from .media import create_media
 
 
 
 
 class ThemeChoiceField(TreeNodeChoiceField):
 class ThemeChoiceField(TreeNodeChoiceField):
@@ -106,8 +107,6 @@ class UploadCssForm(UploadAssetsForm):
         create_css(self.instance, asset)
         create_css(self.instance, asset)
 
 
 
 
-class UploadImagesForm(UploadAssetsForm):
-    assets = forms.ImageField(widget=forms.ClearableFileInput(attrs={"multiple": True}))
-
+class UploadMediaForm(UploadAssetsForm):
     def save_asset(self, asset):
     def save_asset(self, asset):
-        create_image(self.instance, asset)
+        create_media(self.instance, asset)

+ 74 - 0
misago/admin/themes/media.py

@@ -0,0 +1,74 @@
+import io
+
+from PIL import Image
+from django.core.files.images import ImageFile
+
+from .utils import get_file_hash
+
+IMAGE_TYPES = ("image/gif", "image/png", "image/jpeg", "image/bmp", "image/webp")
+THUMBNAIL_SIZE = (32, 32)
+
+
+def create_media(theme, media):
+    if media_exists(theme, media):
+        delete_media(theme, media)
+
+    if media_is_image(media):
+        save_image(theme, media)
+    else:
+        save_media(theme, media)
+
+
+def media_exists(theme, media):
+    return theme.media.filter(name=media.name).exists()
+
+
+def delete_media(theme, media):
+    theme.media.get(name=media.name).delete()
+
+
+def media_is_image(image):
+    return image.content_type in IMAGE_TYPES
+
+
+def save_image(theme, image):
+    try:
+        img = Image.open(image.file)
+    except Exception:
+        return
+    else:
+        width, height = img.size
+
+    theme.media.create(
+        name=image.name,
+        file=image,
+        hash=get_file_hash(image),
+        type=image.content_type,
+        width=width,
+        height=height,
+        size=image.size,
+        thumbnail=get_image_thumbnail(img, image.name),
+    )
+
+    img.close()
+
+
+def get_image_thumbnail(image, src_name):
+    img = image.copy()
+    img.thumbnail(THUMBNAIL_SIZE)
+    file = io.BytesIO()
+    img.save(file, format="png")
+    img.close()
+
+    filename = ".".join(src_name.split(".")[:1])
+    return ImageFile(file, name="thumb_%s.png" % filename)
+
+
+def save_media(theme, media):
+    theme.media.create(
+        name=media.name,
+        file=media,
+        hash=get_file_hash(media),
+        type=media.content_type,
+        size=media.size,
+    )

+ 65 - 0
misago/admin/themes/tests/test_uploading_media.py

@@ -0,0 +1,65 @@
+import os
+
+import pytest
+from django.urls import reverse
+
+TESTS_DIR = os.path.dirname(os.path.abspath(__file__))
+
+
+@pytest.fixture
+def font_file():
+    return os.path.join(TESTS_DIR, "font", "Lato.ttf")
+
+
+@pytest.fixture
+def text_file():
+    return os.path.join(TESTS_DIR, "font", "OFL.txt")
+
+
+@pytest.fixture
+def png_file():
+    return os.path.join(TESTS_DIR, "images", "test.png")
+
+
+@pytest.fixture
+def svg_file():
+    return os.path.join(TESTS_DIR, "images", "test.svg")
+
+
+@pytest.fixture
+def upload(admin_client):
+    def post_upload(theme, asset_files):
+        url = reverse(
+            "misago:admin:appearance:themes:upload-media", kwargs={"pk": theme.pk}
+        )
+        if asset_files:
+            data = asset_files if isinstance(asset_files, list) else [asset_files]
+        else:
+            data = None
+        return admin_client.post(url, {"assets": data})
+
+    return post_upload
+
+
+def test_font_file_can_be_uploaded(upload, theme, font_file):
+    with open(font_file, "rb") as fp:
+        upload(theme, fp)
+        assert theme.media.exists()
+
+
+def test_text_file_can_be_uploaded(upload, theme, text_file):
+    with open(text_file) as fp:
+        upload(theme, fp)
+        assert theme.media.exists()
+
+
+def test_png_file_can_be_uploaded(upload, theme, png_file):
+    with open(png_file, "rb") as fp:
+        upload(theme, fp)
+        assert theme.media.exists()
+
+
+def test_svg_file_can_be_uploaded(upload, theme, svg_file):
+    with open(svg_file) as fp:
+        upload(theme, fp)
+        assert theme.media.exists()

+ 10 - 0
misago/admin/themes/utils.py

@@ -0,0 +1,10 @@
+import hashlib
+
+
+def get_file_hash(file):
+    if file.size is None:
+        return "00000000"
+    file_hash = hashlib.md5()
+    for chunk in file.chunks():
+        file_hash.update(chunk)
+    return file_hash.hexdigest()[:8]

+ 9 - 14
misago/admin/themes/views.py

@@ -7,7 +7,7 @@ from django.utils.translation import gettext, gettext_lazy as _
 
 
 from ...themes.models import Theme
 from ...themes.models import Theme
 from ..views import generic
 from ..views import generic
-from .forms import ThemeForm, UploadCssForm, UploadImagesForm
+from .forms import ThemeForm, UploadCssForm, UploadMediaForm
 
 
 
 
 class ThemeAdmin(generic.AdminBaseMixin):
 class ThemeAdmin(generic.AdminBaseMixin):
@@ -123,9 +123,9 @@ class UploadThemeCss(UploadThemeAssets):
     form = UploadCssForm
     form = UploadCssForm
 
 
 
 
-class UploadThemeImages(UploadThemeAssets):
+class UploadThemeMedia(UploadThemeAssets):
     message_success = _("New media files have been added to the style.")
     message_success = _("New media files have been added to the style.")
-    form = UploadImagesForm
+    form = UploadMediaForm
 
 
 
 
 class DeleteThemeAssets(ThemeAssetsActionAdmin, generic.TargetedView):
 class DeleteThemeAssets(ThemeAssetsActionAdmin, generic.TargetedView):
@@ -137,7 +137,7 @@ class DeleteThemeAssets(ThemeAssetsActionAdmin, generic.TargetedView):
         if items:
         if items:
             queryset = getattr(theme, self.queryset_attr)
             queryset = getattr(theme, self.queryset_attr)
             for item in items:
             for item in items:
-                self.delete_item(queryset, item)
+                self.delete_asset(queryset, item)
 
 
             messages.success(request, self.message_submit)
             messages.success(request, self.message_submit)
 
 
@@ -147,7 +147,7 @@ class DeleteThemeAssets(ThemeAssetsActionAdmin, generic.TargetedView):
         except (ValueError, TypeError):
         except (ValueError, TypeError):
             pass
             pass
 
 
-    def delete_item(self, queryset, item):
+    def delete_asset(self, queryset, item):
         try:
         try:
             queryset.get(pk=item).delete()
             queryset.get(pk=item).delete()
         except ObjectDoesNotExist:
         except ObjectDoesNotExist:
@@ -155,15 +155,10 @@ class DeleteThemeAssets(ThemeAssetsActionAdmin, generic.TargetedView):
 
 
 
 
 class DeleteThemeCss(DeleteThemeAssets):
 class DeleteThemeCss(DeleteThemeAssets):
-    message_submit = _("Selected css files have been deleted.")
+    message_submit = _("Selected CSS files have been deleted.")
     queryset_attr = "css"
     queryset_attr = "css"
 
 
 
 
-class DeleteThemeImages(DeleteThemeAssets):
-    message_submit = _("Selected images have been deleted.")
-    queryset_attr = "images"
-
-
-class DeleteThemeFonts(DeleteThemeAssets):
-    message_submit = _("Selected font files have been deleted.")
-    queryset_attr = "fonts"
+class DeleteThemeMedia(DeleteThemeAssets):
+    message_submit = _("Selected media have been deleted.")
+    queryset_attr = "media"

+ 0 - 47
misago/templates/misago/admin/themes/assets/fonts.html

@@ -1,47 +0,0 @@
-{% load i18n %}
-<div class="table-panel">
-  <div class="panel-heading">
-    <h3 class="panel-title">
-      {% trans "Fonts" %}
-    </h3>
-    <button type="button" class="btn btn-default btn-sm pull-right">
-      <span class="fa fa-upload"></span>
-      {% trans "Upload fonts" %}
-    </button>
-  </div>
-  {% with theme.fonts.all as fonts %}
-    <form action="{% url 'misago:admin:appearance:themes:delete-fonts' pk=theme.pk %}" method="post">
-      <table class="table">
-        <tr>
-          <th>{% trans "Name" %}</th>
-          <th style="width: 1%;">&nbsp;</th>
-          <th style="width: 1%;">
-            <input type="checkbox" {% if not fonts %}disabled{% endif %}>
-          </th>
-        </tr>
-        {% for item in fonts %}
-          <tr>
-            <td>
-              <strong>{{ item }}</strong>
-            </td>
-            <td style="width: 1%;">
-              <input type="checkbox" name="item" value="{{ item.pk }}">
-            </td>
-          </tr>
-        {% empty %}
-          <tr class="message-row">
-            <td colspan="3">{% trans "This theme has no fonts." %}</td>
-          </tr>
-        {% endfor %}
-      </table>
-      {% if fonts %}
-        <div class="panel-footer text-right">
-          <button class="btn btn-danger btn-sm" disabled>
-            <span class="fa fa-remove"></span>
-            {% trans "Delete selected" %}
-          </button>
-        </div>
-      {% endif %}
-    </form>
-  {% endwith %}
-</div><!-- /.table-panel -->

+ 2 - 3
misago/templates/misago/admin/themes/assets/list.html

@@ -61,8 +61,7 @@
 
 
 {% block view %}
 {% block view %}
 {% include "misago/admin/themes/assets/css.html" %}
 {% include "misago/admin/themes/assets/css.html" %}
-{% include "misago/admin/themes/assets/images.html" %}
-{% include "misago/admin/themes/assets/fonts.html" %}
+{% include "misago/admin/themes/assets/media.html" %}
 {% endblock view %}
 {% endblock view %}
 
 
 
 
@@ -70,7 +69,7 @@
 {{ block.super }}
 {{ block.super }}
 
 
 {% include "misago/admin/themes/assets/upload-css.html" %}
 {% include "misago/admin/themes/assets/upload-css.html" %}
-{% include "misago/admin/themes/assets/upload-images.html" %}
+{% include "misago/admin/themes/assets/upload-media.html" %}
 {% endblock content%}
 {% endblock content%}
 
 
 {% block javascripts %}
 {% block javascripts %}

+ 25 - 13
misago/templates/misago/admin/themes/assets/images.html → misago/templates/misago/admin/themes/assets/media.html

@@ -2,15 +2,15 @@
 <div class="table-panel">
 <div class="table-panel">
   <div class="panel-heading">
   <div class="panel-heading">
     <h3 class="panel-title">
     <h3 class="panel-title">
-      {% trans "Images" %}
+      {% trans "Media" %}
     </h3>
     </h3>
-    <button type="button" class="btn btn-default btn-sm pull-right" data-toggle="modal" data-target="#uploadImages">
+    <button type="button" class="btn btn-default btn-sm pull-right" data-toggle="modal" data-target="#uploadMedia">
       <span class="fa fa-upload"></span>
       <span class="fa fa-upload"></span>
-      {% trans "Upload images" %}
+      {% trans "Upload media" %}
     </button>
     </button>
   </div>
   </div>
-  {% with theme.images.all as images %}
-    <form action="{% url 'misago:admin:appearance:themes:delete-images' pk=theme.pk %}" method="post">
+  {% with theme.media.all as media %}
+    <form action="{% url 'misago:admin:appearance:themes:delete-media' pk=theme.pk %}" method="post">
       {% csrf_token %}
       {% csrf_token %}
       <table class="table">
       <table class="table">
         <tr>
         <tr>
@@ -21,15 +21,21 @@
           <th>{% trans "Type" %}</th>
           <th>{% trans "Type" %}</th>
           <th>{% trans "Dimensions" %}</th>
           <th>{% trans "Dimensions" %}</th>
           <th style="width: 1%;">
           <th style="width: 1%;">
-            <input type="checkbox" {% if not images %}disabled{% endif %}>
+            <input type="checkbox" {% if not media %}disabled{% endif %}>
           </th>
           </th>
         </tr>
         </tr>
-        {% for item in images %}
+        {% for item in media %}
           <tr>
           <tr>
             <td style="width: 1%;">
             <td style="width: 1%;">
-              <a class="img-preview tooltip-top" href="{{ item.file.url }}" target="blank" title="{% trans 'Preview' %}">
-                <span style="background-image: url({{ item.thumbnail.url }});"></span>
-              </a>
+              {% if item.thumbnail %}
+                <a class="img-preview tooltip-top" href="{{ item.file.url }}" target="blank" title="{% trans 'Preview' %}">
+                  <span style="background-image: url({{ item.thumbnail.url }});"></span>
+                </a>
+              {% else %}
+                <a class="btn btn-default btn-sm tooltip-top" href="{{ item.file.url }}" target="blank" title="{% trans 'Preview' %}">
+                  <span class="fa fa-share-square-o"></span>
+                </a>
+              {% endif %}
             </td>
             </td>
             <td>
             <td>
               <strong>{{ item }}</strong>
               <strong>{{ item }}</strong>
@@ -39,18 +45,24 @@
             </td>
             </td>
             <td>{{ item.size|filesizeformat }}</td>
             <td>{{ item.size|filesizeformat }}</td>
             <td>{{ item.type }}</td>
             <td>{{ item.type }}</td>
-            <td>{{ item.width }}&times;{{ item.height }}</td>
+            <td>
+              {% if item.width and item.height %}
+                {{ item.width }}&times;{{ item.height }}
+              {% else %}
+                &nbsp;
+              {% endif %}
+            </td>
             <td style="width: 1%;">
             <td style="width: 1%;">
               <input type="checkbox" name="item" value="{{ item.pk }}">
               <input type="checkbox" name="item" value="{{ item.pk }}">
             </td>
             </td>
           </tr>
           </tr>
         {% empty %}
         {% empty %}
           <tr class="message-row">
           <tr class="message-row">
-            <td colspan="8">{% trans "This theme has no images." %}</td>
+            <td colspan="8">{% trans "This theme has no media." %}</td>
           </tr>
           </tr>
         {% endfor %}
         {% endfor %}
       </table>
       </table>
-      {% if images %}
+      {% if media %}
         <div class="panel-footer text-right">
         <div class="panel-footer text-right">
           <button class="btn btn-danger btn-sm" disabled>
           <button class="btn btn-danger btn-sm" disabled>
             <span class="fa fa-remove"></span>
             <span class="fa fa-remove"></span>

+ 9 - 6
misago/templates/misago/admin/themes/assets/upload-images.html → misago/templates/misago/admin/themes/assets/upload-media.html

@@ -1,19 +1,22 @@
 {% load i18n %}
 {% load i18n %}
-<div class="modal fade" id="uploadImages" tabindex="-1" role="dialog">
+<div class="modal fade" id="uploadMedia" tabindex="-1" role="dialog">
   <div class="modal-dialog" role="document">
   <div class="modal-dialog" role="document">
     <div class="modal-content">
     <div class="modal-content">
       <div class="modal-header">
       <div class="modal-header">
         <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
         <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
-        <h4 class="modal-title">{% trans "Upload images" %}</h4>
+        <h4 class="modal-title">{% trans "Upload media" %}</h4>
       </div>
       </div>
-      <form action="{% url 'misago:admin:appearance:themes:upload-images' pk=theme.pk %}" method="post" enctype="multipart/form-data">
+      <form action="{% url 'misago:admin:appearance:themes:upload-media' pk=theme.pk %}" method="post" enctype="multipart/form-data">
         {% csrf_token %}
         {% csrf_token %}
         <div class="modal-body">
         <div class="modal-body">
           <div class="form-group">
           <div class="form-group">
-            <label for="uploadImagesInput">{% trans "Select image files to upload" %}:</label>
-            <input type="file" class="form-control" id="uploadImagesInput" name="assets" multiple>
+            <label for="uploadMediaInput">{% trans "Select media files to upload" %}:</label>
+            <input type="file" class="form-control" id="uploadMediaInput" name="assets" multiple>
             <p class="help-block">
             <p class="help-block">
-              {% trans 'Uploaded images will be placed in "img" folder. Image urls in CSS should be prefixed with "img/".' %}
+              {% trans "Media files are primarily image and font files that can be linked to from the CSS, but files of any type can be uploaded using this option." %}
+            </p>
+            <p class="help-block">
+              {% trans 'Theme\'s CSS that use the "url()" to point to media files will be updated automatically.' %}
             </p>
             </p>
           </div>
           </div>
         </div>
         </div>

+ 10 - 46
misago/themes/migrations/0001_initial.py

@@ -1,4 +1,4 @@
-# Generated by Django 1.11.17 on 2018-12-29 16:02
+# Generated by Django 1.11.17 on 2018-12-29 20:42
 from django.db import migrations, models
 from django.db import migrations, models
 import django.db.models.deletion
 import django.db.models.deletion
 import misago.themes.uploadto
 import misago.themes.uploadto
@@ -54,33 +54,7 @@ class Migration(migrations.Migration):
             options={"ordering": ["order"]},
             options={"ordering": ["order"]},
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
-            name="Font",
-            fields=[
-                (
-                    "id",
-                    models.AutoField(
-                        auto_created=True,
-                        primary_key=True,
-                        serialize=False,
-                        verbose_name="ID",
-                    ),
-                ),
-                ("name", models.CharField(max_length=255)),
-                (
-                    "file",
-                    models.FileField(
-                        max_length=255, upload_to=misago.themes.uploadto.upload_font_to
-                    ),
-                ),
-                ("hash", models.CharField(max_length=12)),
-                ("type", models.CharField(max_length=255)),
-                ("size", models.PositiveIntegerField()),
-                ("modified_on", models.DateTimeField(auto_now=True)),
-            ],
-            options={"ordering": ["name"]},
-        ),
-        migrations.CreateModel(
-            name="Image",
+            name="Media",
             fields=[
             fields=[
                 (
                 (
                     "id",
                     "id",
@@ -95,22 +69,21 @@ class Migration(migrations.Migration):
                 (
                 (
                     "file",
                     "file",
                     models.ImageField(
                     models.ImageField(
-                        height_field="height",
-                        max_length=255,
-                        upload_to=misago.themes.uploadto.upload_image_to,
-                        width_field="width",
+                        max_length=255, upload_to=misago.themes.uploadto.upload_media_to
                     ),
                     ),
                 ),
                 ),
                 ("hash", models.CharField(max_length=8)),
                 ("hash", models.CharField(max_length=8)),
                 ("type", models.CharField(max_length=255)),
                 ("type", models.CharField(max_length=255)),
-                ("width", models.PositiveIntegerField()),
-                ("height", models.PositiveIntegerField()),
+                ("width", models.PositiveIntegerField(default=0)),
+                ("height", models.PositiveIntegerField(default=0)),
                 ("size", models.PositiveIntegerField()),
                 ("size", models.PositiveIntegerField()),
                 (
                 (
                     "thumbnail",
                     "thumbnail",
                     models.ImageField(
                     models.ImageField(
+                        blank=True,
                         max_length=255,
                         max_length=255,
-                        upload_to=misago.themes.uploadto.upload_image_thumbnail_to,
+                        null=True,
+                        upload_to=misago.themes.uploadto.upload_media_thumbnail_to,
                     ),
                     ),
                 ),
                 ),
                 ("modified_on", models.DateTimeField(auto_now=True)),
                 ("modified_on", models.DateTimeField(auto_now=True)),
@@ -160,20 +133,11 @@ class Migration(migrations.Migration):
             options={"abstract": False},
             options={"abstract": False},
         ),
         ),
         migrations.AddField(
         migrations.AddField(
-            model_name="image",
-            name="theme",
-            field=models.ForeignKey(
-                on_delete=django.db.models.deletion.PROTECT,
-                related_name="images",
-                to="misago_themes.Theme",
-            ),
-        ),
-        migrations.AddField(
-            model_name="font",
+            model_name="media",
             name="theme",
             name="theme",
             field=models.ForeignKey(
             field=models.ForeignKey(
                 on_delete=django.db.models.deletion.PROTECT,
                 on_delete=django.db.models.deletion.PROTECT,
-                related_name="fonts",
+                related_name="media",
                 to="misago_themes.Theme",
                 to="misago_themes.Theme",
             ),
             ),
         ),
         ),

+ 12 - 36
misago/themes/models.py

@@ -6,9 +6,8 @@ from .uploadto import (
     generate_theme_dirname,
     generate_theme_dirname,
     upload_css_source_to,
     upload_css_source_to,
     upload_css_to,
     upload_css_to,
-    upload_font_to,
-    upload_image_to,
-    upload_image_thumbnail_to,
+    upload_media_to,
+    upload_media_thumbnail_to,
 )
 )
 
 
 
 
@@ -72,43 +71,19 @@ class Css(models.Model):
         return self.name
         return self.name
 
 
 
 
-class Font(models.Model):
-    theme = models.ForeignKey(Theme, on_delete=models.PROTECT, related_name="fonts")
+class Media(models.Model):
+    theme = models.ForeignKey(Theme, on_delete=models.PROTECT, related_name="media")
 
 
     name = models.CharField(max_length=255)
     name = models.CharField(max_length=255)
-    file = models.FileField(upload_to=upload_font_to, max_length=255)
-    hash = models.CharField(max_length=12)
-    type = models.CharField(max_length=255)
-    size = models.PositiveIntegerField()
-    modified_on = models.DateTimeField(auto_now=True)
-
-    class Meta:
-        ordering = ["name"]
-
-    def delete(self, *args, **kwargs):
-        self.file.delete(save=False)
-        super().delete(*args, **kwargs)
-
-    def __str__(self):
-        return self.name
-
-
-class Image(models.Model):
-    theme = models.ForeignKey(Theme, on_delete=models.PROTECT, related_name="images")
-
-    name = models.CharField(max_length=255)
-    file = models.ImageField(
-        upload_to=upload_image_to,
-        max_length=255,
-        width_field="width",
-        height_field="height",
-    )
+    file = models.ImageField(upload_to=upload_media_to, max_length=255)
     hash = models.CharField(max_length=8)
     hash = models.CharField(max_length=8)
     type = models.CharField(max_length=255)
     type = models.CharField(max_length=255)
-    width = models.PositiveIntegerField()
-    height = models.PositiveIntegerField()
+    width = models.PositiveIntegerField(default=0)
+    height = models.PositiveIntegerField(default=0)
     size = models.PositiveIntegerField()
     size = models.PositiveIntegerField()
-    thumbnail = models.ImageField(upload_to=upload_image_thumbnail_to, max_length=255)
+    thumbnail = models.ImageField(
+        upload_to=upload_media_thumbnail_to, max_length=255, null=True, blank=True
+    )
     modified_on = models.DateTimeField(auto_now=True)
     modified_on = models.DateTimeField(auto_now=True)
 
 
     class Meta:
     class Meta:
@@ -116,7 +91,8 @@ class Image(models.Model):
 
 
     def delete(self, *args, **kwargs):
     def delete(self, *args, **kwargs):
         self.file.delete(save=False)
         self.file.delete(save=False)
-        self.thumbnail.delete(save=False)
+        if self.thumbnail:
+            self.thumbnail.delete(save=False)
         super().delete(*args, **kwargs)
         super().delete(*args, **kwargs)
 
 
     def __str__(self):
     def __str__(self):

+ 2 - 7
misago/themes/uploadto.py

@@ -15,17 +15,12 @@ def upload_css_to(instance, filename):
     return "themes/%s/css/%s" % (instance.theme.dirname, filename)
     return "themes/%s/css/%s" % (instance.theme.dirname, filename)
 
 
 
 
-def upload_font_to(instance, filename):
+def upload_media_to(instance, filename):
     filename = add_hash_to_filename(instance.hash, filename)
     filename = add_hash_to_filename(instance.hash, filename)
     return "themes/%s/media/%s" % (instance.theme.dirname, filename)
     return "themes/%s/media/%s" % (instance.theme.dirname, filename)
 
 
 
 
-def upload_image_to(instance, filename):
-    filename = add_hash_to_filename(instance.hash, filename)
-    return "themes/%s/media/%s" % (instance.theme.dirname, filename)
-
-
-def upload_image_thumbnail_to(instance, filename):
+def upload_media_thumbnail_to(instance, filename):
     return "themes/%s/media/%s" % (instance.theme.dirname, filename)
     return "themes/%s/media/%s" % (instance.theme.dirname, filename)