rafalp 6 лет назад
Родитель
Сommit
da42477572

+ 40 - 1
misago/admin/admin.py

@@ -1,10 +1,33 @@
+from django.conf.urls import url
 from django.utils.deprecation import MiddlewareMixin
 from django.utils.translation import gettext_lazy as _
 
+from .themes.views import (
+    ActivateTheme,
+    DeleteTheme,
+    EditTheme,
+    NewTheme,
+    ThemeAssets,
+    ThemesList,
+)
+
 
 class MisagoAdminExtension(MiddlewareMixin):
     def register_urlpatterns(self, urlpatterns):
-        pass
+        # Appearance section
+        urlpatterns.namespace(r"^appearance/", "appearance")
+
+        # Themes
+        urlpatterns.namespace(r"^themes/", "themes", "appearance")
+        urlpatterns.patterns(
+            "appearance:themes",
+            url(r"^$", ThemesList.as_view(), name="index"),
+            url(r"^new/$", NewTheme.as_view(), name="new"),
+            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"^assets/(?P<pk>\d+)/$", ThemeAssets.as_view(), name="assets"),
+        )
 
     def register_navigation_nodes(self, site):
         site.add_node(
@@ -13,3 +36,19 @@ class MisagoAdminExtension(MiddlewareMixin):
             parent="misago:admin",
             link="misago:admin:index",
         )
+
+        site.add_node(
+            name=_("Appearance"),
+            icon="fa fa-paint-brush",
+            parent="misago:admin",
+            namespace="misago:admin:appearance",
+            link="misago:admin:appearance:themes:index",
+        )
+
+        site.add_node(
+            name=_("Themes"),
+            icon="fa fa-tint",
+            parent="misago:admin:appearance",
+            namespace="misago:admin:appearance:themes",
+            link="misago:admin:appearance:themes:index",
+        )

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


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


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

@@ -0,0 +1,42 @@
+from django import forms
+from django.utils.html import conditional_escape, mark_safe
+from django.utils.translation import gettext_lazy as _
+from mptt.forms import TreeNodeChoiceField
+
+from ...theming.models import Theme
+from ..forms import YesNoSwitch
+
+
+class ThemeChoiceField(TreeNodeChoiceField):
+    level_indicator = "- - "
+
+    def __init__(self, *args, **kwargs):
+        kwargs.setdefault("queryset", Theme.objects.all())
+        kwargs.setdefault("empty_label", _("No parent"))
+        super().__init__(*args, **kwargs)
+
+
+class ThemeForm(forms.ModelForm):
+    name = forms.CharField(label=_("Name"))
+    parent = ThemeChoiceField(label=_("Parent"), required=False)
+    version = forms.CharField(label=_("Version"), required=False)
+    author = forms.CharField(label=_("Author(s)"), required=False)
+    url = forms.URLField(label=_("Url"), required=False)
+
+    class Meta:
+        model = Theme
+        fields = ["name", "parent", "version", "author", "url"]
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.limit_parent_choices()
+
+    def limit_parent_choices(self):
+        if not self.instance or not self.instance.pk:
+            return
+
+        self.fields["parent"].queryset = Theme.objects.exclude(
+            tree_id=self.instance.tree_id,
+            lft__gte=self.instance.lft,
+            rght__lte=self.instance.rght,
+        )

+ 76 - 0
misago/admin/themes/views.py

@@ -0,0 +1,76 @@
+from django.contrib import messages
+from django.core.exceptions import PermissionDenied
+from django.shortcuts import redirect
+from django.urls import reverse
+from django.utils.translation import gettext, gettext_lazy as _
+
+from ...theming.models import Theme
+from ..views import generic
+from .forms import ThemeForm
+
+
+class ThemeAdmin(generic.AdminBaseMixin):
+    root_link = "misago:admin:appearance:themes:index"
+    model = Theme
+    form = ThemeForm
+    templates_dir = "misago/admin/themes"
+    message_404 = _("Requested theme does not exist.")
+
+
+class ThemesList(ThemeAdmin, generic.ListView):
+    pass
+
+
+class NewTheme(ThemeAdmin, generic.ModelFormView):
+    message_submit = _('New theme "%(name)s" has been saved.')
+
+    def initialize_form(self, form, request, _):
+        if request.method == "POST":
+            return form(request.POST, request.FILES)
+
+        try:
+            initial = {"parent": int(request.GET.get("parent"))}
+        except (TypeError, ValueError):
+            initial = {}
+
+        return form(initial=initial)
+
+
+class EditTheme(ThemeAdmin, generic.ModelFormView):
+    message_submit = _('Theme "%(name)s" has been updated.')
+
+    def check_permissions(self, request, target):
+        if target.is_default:
+            return gettext("Default theme can't be edited.")
+
+
+class DeleteTheme(ThemeAdmin, generic.ModelFormView):
+    message_submit = _('Theme "%(name)s" has been deleted.')
+
+    def check_permissions(self, request, target):
+        if target.is_default:
+            return gettext("Default theme can't be deleted.")
+
+
+class ActivateTheme(ThemeAdmin, generic.ButtonView):
+    def button_action(self, request, target):
+        set_theme_as_active(request, target)
+
+        message = gettext('Active theme has been changed to "%(name)s".')
+        messages.success(request, message % {"name": target})
+
+
+class ThemeAssets(ThemeAdmin, generic.TargetedView):
+    template = "assets.html"
+
+    def check_permissions(self, request, theme):
+        if theme.is_default:
+            return gettext("Default theme assets can't be edited.")
+
+    def real_dispatch(self, request, theme):
+        return self.render(request, {"theme": theme})
+
+
+def set_theme_as_active(request, theme):
+    Theme.objects.update(is_active=False)
+    Theme.objects.filter(pk=theme.pk).update(is_active=True)

+ 21 - 21
misago/admin/views/generic/formsbuttons.py

@@ -8,27 +8,6 @@ from .base import AdminView
 class TargetedView(AdminView):
     is_atomic = True
 
-    def check_permissions(self, request, target):
-        pass
-
-    def get_target(self, kwargs):
-        if len(kwargs) != 1:
-            return self.get_model()()
-
-        select_for_update = self.get_model().objects
-        if self.is_atomic:
-            select_for_update = select_for_update.select_for_update()
-        # Does not work on Python 3:
-        # return select_for_update.get(pk=kwargs[kwargs.keys()[0]])
-        (pk,) = kwargs.values()
-        return select_for_update.get(pk=pk)
-
-    def get_target_or_none(self, request, kwargs):
-        try:
-            return self.get_target(kwargs)
-        except self.get_model().DoesNotExist:
-            return None
-
     def dispatch(self, request, *args, **kwargs):
         if self.is_atomic:
             with transaction.atomic():
@@ -50,6 +29,27 @@ class TargetedView(AdminView):
 
         return self.real_dispatch(request, target)
 
+    def get_target_or_none(self, request, kwargs):
+        try:
+            return self.get_target(kwargs)
+        except self.get_model().DoesNotExist:
+            return None
+
+    def get_target(self, kwargs):
+        if len(kwargs) != 1:
+            return self.get_model()()
+
+        select_for_update = self.get_model().objects
+        if self.is_atomic:
+            select_for_update = select_for_update.select_for_update()
+        # Does not work on Python 3:
+        # return select_for_update.get(pk=kwargs[kwargs.keys()[0]])
+        (pk,) = kwargs.values()
+        return select_for_update.get(pk=pk)
+
+    def check_permissions(self, request, target):
+        pass
+
     def real_dispatch(self, request, target):
         pass
 

+ 1 - 0
misago/templates/misago/admin/base_thin.html

@@ -25,6 +25,7 @@
     <link rel="apple-touch-icon" sizes="180x180" href="{% static 'apple-touch-icon-180.png' %}">
     <link rel="shortcut icon" href="{% static 'favicon.ico' %}">
     <link rel="icon" sizes="16x16 32x32" href="{% static 'favicon.ico' %}">
+    {% block extra-head %}{% endblock extra-head %}
   </head>
   <body>
 

+ 117 - 0
misago/templates/misago/admin/themes/assets.html

@@ -0,0 +1,117 @@
+{% extends "misago/admin/generic/base.html" %}
+{% load i18n %}
+
+
+{% block title %}
+{% trans "Assets" %} | {{ theme }} | {{ active_link.name }} | {{ block.super }}
+{% endblock title%}
+
+
+{% block extra-head %}
+<style>
+  .panel-heading .btn-sm {
+    box-shadow: none !important;
+    margin: 0px;
+    margin-top: -23px;
+    margin-left: 14px;
+  }
+</style>
+{% endblock extra-head %}
+
+
+{% block page-header %}
+{{ block.super }}
+<div class="sub">
+  <span class="fa fa-chevron-right"></span>
+  {{ theme }}
+</div>
+{% endblock page-header %}
+
+
+{% block view %}
+<div class="table-panel">
+  <div class="panel-heading">
+    <h3 class="panel-title">
+      {% trans "CSS files" %}
+    </h3>
+    <button type="button" class="btn btn-default btn-sm pull-right">
+      <span class="fa fa-upload"></span>
+      {% trans "Upload CSS" %}
+    </button>
+    <button type="button" class="btn btn-default btn-sm pull-right">
+      <span class="fa fa-link"></span>
+      {% trans "Link" %}
+    </button>
+    <button type="button" class="btn btn-default btn-sm pull-right">
+      <span class="fa fa-file-text"></span>
+      {% trans "Create" %}
+    </button>
+  </div>
+  <table class="table">
+    <tr>
+      <th>{% trans "Filename" %}</th>
+    </tr>
+    {% for item in theme.css.all %}
+      <tr>
+        <td>{{ item }}</td>
+      </tr>
+    {% empty %}
+      <tr class="message-row">
+        <td>{% trans "This theme has no CSS files." %}</td>
+      </tr>
+    {% endfor %}
+  </table>
+</div><!-- /.table-panel -->
+
+<div class="table-panel">
+  <div class="panel-heading">
+    <h3 class="panel-title">
+      {% trans "Images" %}
+    </h3>
+    <button type="button" class="btn btn-default btn-sm pull-right">
+      <span class="fa fa-upload"></span>
+      {% trans "Upload images" %}
+    </button>
+  </div>
+  <table class="table">
+    <tr>
+      <th>{% trans "Filename" %}</th>
+    </tr>
+    {% for item in theme.images.all %}
+      <tr>
+        <td>{{ item }}</td>
+      </tr>
+    {% empty %}
+      <tr class="message-row">
+        <td>{% trans "This theme has no images." %}</td>
+      </tr>
+    {% endfor %}
+  </table>
+</div><!-- /.table-panel -->
+
+<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>
+  <table class="table">
+    <tr>
+      <th>{% trans "Filename" %}</th>
+    </tr>
+    {% for item in theme.fonts.all %}
+      <tr>
+        <td>{{ item }}</td>
+      </tr>
+    {% empty %}
+      <tr class="message-row">
+        <td>{% trans "This theme has no fonts." %}</td>
+      </tr>
+    {% endfor %}
+  </table>
+</div><!-- /.table-panel -->
+{% endblock view %}

+ 64 - 0
misago/templates/misago/admin/themes/form.html

@@ -0,0 +1,64 @@
+{% extends "misago/admin/generic/form.html" %}
+{% load i18n misago_admin_form %}
+
+
+{% block title %}
+{% if target.pk %}
+  {{ target }}
+{% else %}
+  {% trans "New theme" %}
+{% endif %} | {{ active_link.name }} | {{ block.super }}
+{% endblock title %}
+
+
+{% block page-target %}
+{% if target.pk %}
+  {{ target }}
+{% else %}
+  {% trans "New theme" %}
+{% endif %}
+{% endblock page-target %}
+
+
+{% block form-header %}
+<h1>
+  {% if target.pk %}
+    {{ target }}
+  {% else %}
+    {% trans "New theme" %}
+  {% endif %}
+</h1>
+{% endblock %}
+
+
+{% block form-extra %}
+class="form-horizontal"
+{% endblock form-extra%}
+
+
+{% block form-body %}
+<div class="form-body">
+  {% with label_class="col-md-3" field_class="col-md-9" %}
+    <fieldset>
+      <legend>{% trans "Settings" %}</legend>
+
+      {% form_row form.parent label_class field_class %}
+      {% form_row form.name label_class field_class %}
+
+    </fieldset>
+    <fieldset>
+      <legend>{% trans "Additional information (optional)" %}</legend>
+
+      {% form_row form.version label_class field_class %}
+      {% form_row form.author label_class field_class %}
+      {% form_row form.url label_class field_class %}
+
+    </fieldset>
+  {% endwith %}
+</div>
+{% endblock form-body %}
+
+
+{% block form-footer-class %}
+col-md-offset-3
+{% endblock form-footer-class %}

+ 105 - 0
misago/templates/misago/admin/themes/list.html

@@ -0,0 +1,105 @@
+{% extends "misago/admin/generic/list.html" %}
+{% load i18n %}
+
+
+{% block page-actions %}
+<div class="page-actions">
+  <a href="{% url 'misago:admin:appearance:themes:new' %}" class="btn btn-success">
+    <span class="fa fa-plus-circle"></span>
+    {% trans "New theme" %}
+  </a>
+</div>
+{% endblock %}
+
+
+{% block table-header %}
+<th>{% trans "Theme" %}</th>
+<th>&nbsp;</th>
+<th>&nbsp;</th>
+<th>&nbsp;</th>
+<th>&nbsp;</th>
+<th>&nbsp;</th>
+<th>&nbsp;</th>
+<th>&nbsp;</th>
+{% endblock table-header %}
+
+
+{% block table-row %}
+<td class="item-name">
+  {% for i in item.level_range %}
+    &nbsp;&nbsp;&nbsp;&nbsp;
+  {% endfor %}
+  {{ item }}
+  {% if item.version %}
+    <span class="text-muted">
+      {{ item.version }}
+    </span>
+  {% endif %}
+</td>
+<td class="item-name">
+  {% if item.is_active %}
+    <span class="label label-success pull-right" style="margin: 0px;">
+      {% trans "Active" %}
+    </span>
+  {% else %}
+    &nbsp;
+  {% endif %}
+</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)" %}">
+      <span class="fa fa-pencil-square-o"></span>
+    </a>
+  {% else %}
+    <button class="btn" type="button" disabled>
+      <span class="fa fa-pencil-square-o"></span>
+    </button>
+  {% endif %}
+</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" %}">
+      <span class="fa fa-pencil"></span>
+    </a>
+  {% else %}
+    <button class="btn" type="button" disabled>
+      <span class="fa fa-pencil"></span>
+    </button>
+  {% 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" %}">
+    <span class="fa fa-file-o"></span>
+  </a>
+</td>
+<td class="row-action">
+  <a href="{% url 'misago:admin:appearance:themes:edit' pk=item.pk %}" class="btn btn-primary tooltip-top" title="{% trans "Copy" %}">
+    <span class="fa fa-copy"></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" %}">
+        {% csrf_token %}
+        <span class="fa fa-check-square"></span>
+      </button>
+    </form>
+  {% else %}
+    <button class="btn" type="button" disabled>
+      <span class="fa fa-check-square"></span>
+    </button>
+  {% endif %}
+</td>
+<td class="row-action">
+  {% if not item.is_active and not item.is_default %}
+    <a href="{% url 'misago:admin:appearance:themes:delete' pk=item.pk %}" class="btn btn-danger tooltip-top" title="{% trans "Delete" %}">
+      <span class="fa fa-times"></span>
+    </a>
+  {% else %}
+    <button class="btn" type="button" disabled>
+      <span class="fa fa-times"></span>
+    </button>
+  {% endif %}
+</td>
+{% endblock %}

+ 2 - 0
misago/theming/__init__.py

@@ -1 +1,3 @@
 default_app_config = "misago.theming.apps.MisagoThemingConfig"
+
+THEME_CACHE = "theme"

+ 8 - 0
misago/theming/cache.py

@@ -0,0 +1,8 @@
+from django.core.cache import cache
+
+from . import THEME_CACHE
+from ..cache.versions import invalidate_cache
+
+
+def clear_theme_cache():
+    invalidate_cache(THEME_CACHE)

+ 30 - 16
misago/theming/migrations/0001_initial.py

@@ -11,25 +11,39 @@ class Migration(migrations.Migration):
 
     initial = True
 
-    dependencies = [
-    ]
+    dependencies = []
 
     operations = [
         migrations.CreateModel(
-            name='Theme',
+            name="Theme",
             fields=[
-                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('name', models.CharField(max_length=255)),
-                ('is_default', models.BooleanField(default=False)),
-                ('is_active', models.BooleanField(default=False)),
-                ('lft', models.PositiveIntegerField(db_index=True, editable=False)),
-                ('rght', models.PositiveIntegerField(db_index=True, editable=False)),
-                ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
-                ('level', models.PositiveIntegerField(db_index=True, editable=False)),
-                ('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='children', to='misago_theming.Theme')),
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("name", models.CharField(max_length=255)),
+                ("is_default", models.BooleanField(default=False)),
+                ("is_active", models.BooleanField(default=False)),
+                ("lft", models.PositiveIntegerField(db_index=True, editable=False)),
+                ("rght", models.PositiveIntegerField(db_index=True, editable=False)),
+                ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)),
+                ("level", models.PositiveIntegerField(db_index=True, editable=False)),
+                (
+                    "parent",
+                    mptt.fields.TreeForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.PROTECT,
+                        related_name="children",
+                        to="misago_theming.Theme",
+                    ),
+                ),
             ],
-            options={
-                'abstract': False,
-            },
-        ),
+            options={"abstract": False},
+        )
     ]

+ 8 - 4
misago/theming/migrations/0002_create_default_theme.py

@@ -1,6 +1,9 @@
 # Generated by Django 1.11.16 on 2018-12-26 16:11
 from django.db import migrations
 
+from .. import THEME_CACHE
+from ...cache.operations import StartCacheVersioning
+
 
 def create_default_theme(apps, schema_editor):
     Theme = apps.get_model("misago_theming", "Theme")
@@ -17,8 +20,9 @@ def create_default_theme(apps, schema_editor):
 
 class Migration(migrations.Migration):
 
-    dependencies = [
-        ('misago_theming', '0001_initial'),
-    ]
+    dependencies = [("misago_theming", "0001_initial")]
 
-    operations = [migrations.RunPython(create_default_theme)]
+    operations = [
+        StartCacheVersioning(THEME_CACHE),
+        migrations.RunPython(create_default_theme),
+    ]

+ 28 - 0
misago/theming/migrations/0003_auto_20181226_1841.py

@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.17 on 2018-12-26 18:41
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [("misago_theming", "0002_create_default_theme")]
+
+    operations = [
+        migrations.AddField(
+            model_name="theme",
+            name="author",
+            field=models.CharField(blank=True, max_length=255, null=True),
+        ),
+        migrations.AddField(
+            model_name="theme",
+            name="license",
+            field=models.TextField(blank=True, max_length=50000, null=True),
+        ),
+        migrations.AddField(
+            model_name="theme",
+            name="url",
+            field=models.URLField(blank=True, max_length=255, null=True),
+        ),
+    ]

+ 19 - 0
misago/theming/migrations/0004_auto_20181226_2136.py

@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.17 on 2018-12-26 21:36
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [("misago_theming", "0003_auto_20181226_1841")]
+
+    operations = [
+        migrations.RemoveField(model_name="theme", name="license"),
+        migrations.AddField(
+            model_name="theme",
+            name="version",
+            field=models.CharField(blank=True, max_length=255, null=True),
+        ),
+    ]

+ 110 - 0
misago/theming/migrations/0005_css_font_image.py

@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.17 on 2018-12-26 22:22
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [("misago_theming", "0004_auto_20181226_2136")]
+
+    operations = [
+        migrations.CreateModel(
+            name="Css",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("name", models.CharField(max_length=255)),
+                ("url", models.URLField(blank=True, max_length=255, null=True)),
+                (
+                    "file",
+                    models.ImageField(
+                        blank=True, max_length=255, null=True, upload_to=""
+                    ),
+                ),
+                ("size", models.PositiveIntegerField()),
+                ("order", models.IntegerField(default=0)),
+                ("is_enabled", models.BooleanField(default=True)),
+                ("uploaded_on", models.DateTimeField(auto_now_add=True)),
+                ("updated_on", models.DateTimeField(auto_now=True)),
+                (
+                    "theme",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.PROTECT,
+                        related_name="css",
+                        to="misago_theming.Theme",
+                    ),
+                ),
+            ],
+            options={"ordering": ["order"]},
+        ),
+        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="")),
+                ("type", models.CharField(max_length=255)),
+                ("size", models.PositiveIntegerField()),
+                ("uploaded_on", models.DateTimeField(auto_now_add=True)),
+                ("updated_on", models.DateTimeField(auto_now=True)),
+                (
+                    "theme",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.PROTECT,
+                        related_name="fonts",
+                        to="misago_theming.Theme",
+                    ),
+                ),
+            ],
+            options={"ordering": ["name"]},
+        ),
+        migrations.CreateModel(
+            name="Image",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("name", models.CharField(max_length=255)),
+                ("file", models.ImageField(max_length=255, upload_to="")),
+                ("type", models.CharField(max_length=255)),
+                ("width", models.PositiveIntegerField()),
+                ("heigh", models.PositiveIntegerField()),
+                ("size", models.PositiveIntegerField()),
+                ("uploaded_on", models.DateTimeField(auto_now_add=True)),
+                ("updated_on", models.DateTimeField(auto_now=True)),
+                (
+                    "theme",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.PROTECT,
+                        related_name="images",
+                        to="misago_theming.Theme",
+                    ),
+                ),
+            ],
+            options={"ordering": ["name"]},
+        ),
+    ]

+ 61 - 28
misago/theming/models.py

@@ -1,4 +1,5 @@
 from django.db import models
+from django.utils.translation import gettext
 from mptt.models import MPTTModel, TreeForeignKey
 
 
@@ -10,45 +11,77 @@ class Theme(MPTTModel):
     is_default = models.BooleanField(default=False)
     is_active = models.BooleanField(default=False)
 
+    version = models.CharField(max_length=255, null=True, blank=True)
+    author = models.CharField(max_length=255, null=True, blank=True)
+    url = models.URLField(max_length=255, null=True, blank=True)
+
     class MPTTMeta:
         order_insertion_by = ["is_default", "name"]
 
-# class Css(models.Model):
-#     theme = models.ForeignKey(Theme, on_delete=models.PROTECT, related_name="css")
+    def __str__(self):
+        if self.is_default:
+            return gettext("Default Misago Theme")
+        return self.name
+
+    @property
+    def level_range(self):
+        return range(self.level)
 
-#     name = models.CharField(max_length=255)
-#     url = models.UrlField(max_length=255, null=True, blank=True)
-#     file = models.ImageField(max_length=255, null=True, blank=True)
-#     size = models.PositiveIntegerField()
 
-#     order = models.IntegerField(default=0)
-#     is_enabled = models.BooleanField(default=True)
+class Css(models.Model):
+    theme = models.ForeignKey(Theme, on_delete=models.PROTECT, related_name="css")
+
+    name = models.CharField(max_length=255)
+    url = models.URLField(max_length=255, null=True, blank=True)
+    file = models.ImageField(max_length=255, null=True, blank=True)
+    size = models.PositiveIntegerField()
 
-#     uploaded_on = models.DateTimeField(auto_now_add=True)
-#     updated_on = models.DateTimeField(auto_now=True)
+    order = models.IntegerField(default=0)
+    is_enabled = models.BooleanField(default=True)
 
+    uploaded_on = models.DateTimeField(auto_now_add=True)
+    updated_on = models.DateTimeField(auto_now=True)
 
-# class Font(models.Model):
-#     theme = models.ForeignKey(Theme, on_delete=models.PROTECT, related_name="fonts")
+    class Meta:
+        ordering = ["order"]
 
-#     name = models.CharField(max_length=255)
-#     file = models.FileField(max_length=255)
-#     type = models.CharField(max_length=255)
-#     size = models.PositiveIntegerField()
+    def __str__(self):
+        return self.name
 
-#     uploaded_on = models.DateTimeField(auto_now_add=True)
-#     updated_on = models.DateTimeField(auto_now=True)
 
+class Font(models.Model):
+    theme = models.ForeignKey(Theme, on_delete=models.PROTECT, related_name="fonts")
+
+    name = models.CharField(max_length=255)
+    file = models.FileField(max_length=255)
+    type = models.CharField(max_length=255)
+    size = models.PositiveIntegerField()
+
+    uploaded_on = models.DateTimeField(auto_now_add=True)
+    updated_on = models.DateTimeField(auto_now=True)
+
+    class Meta:
+        ordering = ["name"]
+
+    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(max_length=255)
+    type = models.CharField(max_length=255)
+    width = models.PositiveIntegerField()
+    heigh = models.PositiveIntegerField()
+    size = models.PositiveIntegerField()
 
-# class Image(models.Model):
-#     theme = models.ForeignKey(Theme, on_delete=models.PROTECT, related_name="images")
+    uploaded_on = models.DateTimeField(auto_now_add=True)
+    updated_on = models.DateTimeField(auto_now=True)
 
-#     name = models.CharField(max_length=255)
-#     file = models.ImageField(max_length=255)
-#     type = models.CharField(max_length=255)
-#     width = models.PositiveIntegerField()
-#     heigh = PositiveIntegerField()
-#     size = models.PositiveIntegerField()
+    class Meta:
+        ordering = ["name"]
 
-#     uploaded_on = models.DateTimeField(auto_now_add=True)
-#     updated_on = models.DateTimeField(auto_now=True)
+    def __str__(self):
+        return self.name