Browse Source

Delete theme

rafalp 6 years ago
parent
commit
3ad65a4994

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

@@ -93,18 +93,12 @@
 </td>
 <td class="row-action">
   {% if not item.is_active and not item.is_default %}
-    {% if item.is_leaf_node %}
-      <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" %}">
-          <span class="fa fa-times"></span>
-        </button>
-      </form>
-    {% else %}
-      <a href="{% url 'misago:admin:appearance:themes:delete' pk=item.pk %}" class="btn btn-danger tooltip-top" title="{% trans "Delete" %}">
+    <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" %}">
         <span class="fa fa-times"></span>
-      </a>
-    { %endif %}
+      </button>
+    </form>
   {% else %}
     <button class="btn" type="button" disabled>
       <span class="fa fa-times"></span>
@@ -119,7 +113,7 @@
 <script type="text/javascript">
   $(function() {
     $('.delete-prompt').submit(function() {
-      var decision = confirm("{% trans 'Are you sure you want to delete this theme?' %}");
+      var decision = confirm("{% trans 'Are you sure you want to delete this theme? Deleting theme will also delete its child themes.' %}");
       return decision;
     });
   });

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

@@ -16,6 +16,7 @@ class ThemeChoiceField(TreeNodeChoiceField):
     def __init__(self, *args, **kwargs):
         kwargs.setdefault("queryset", Theme.objects.all())
         kwargs.setdefault("empty_label", _("No parent"))
+        kwargs.setdefault("level_indicator", self.level_indicator)
         super().__init__(*args, **kwargs)
 
 

+ 70 - 1
misago/themes/admin/tests/test_deleting_themes.py

@@ -4,7 +4,9 @@ import pytest
 from django.core.files.base import ContentFile
 from django.urls import reverse
 
+from ....cache.test import assert_invalidates_cache
 from ....test import assert_has_error_message
+from ... import THEME_CACHE
 from ...models import Theme, Css, Media
 
 
@@ -69,6 +71,45 @@ def test_theme_image_files_are_deleted_together_with_theme(
     assert not Path(image.thumbnail.path).exists()
 
 
+def test_theme_is_deleted_with_children(admin_client, delete_link, theme):
+    Theme.objects.create(name="Child Theme", parent=theme)
+    admin_client.post(delete_link)
+    assert Theme.objects.count() == 1
+
+
+def test_theme_children_are_deleted_recursively(admin_client, delete_link, theme):
+    child_theme = Theme.objects.create(name="Child Theme", parent=theme)
+    Theme.objects.create(name="Descendant Theme", parent=child_theme)
+    Theme.objects.create(name="Descendant Theme", parent=child_theme)
+
+    admin_client.post(delete_link)
+    assert Theme.objects.count() == 1
+
+
+def test_children_theme_can_be_deleted(admin_client, delete_link, theme, other_theme):
+    theme.move_to(other_theme)
+    theme.save()
+
+    admin_client.post(delete_link)
+    with pytest.raises(Theme.DoesNotExist):
+        theme.refresh_from_db()
+
+
+def test_deleting_children_theme_doesnt_delete_parent_themes(
+    admin_client, delete_link, theme, other_theme
+):
+    theme.move_to(other_theme)
+    theme.save()
+
+    admin_client.post(delete_link)
+    other_theme.refresh_from_db()
+
+
+def test_deleting_theme_invalidates_themes_cache(admin_client, delete_link):
+    with assert_invalidates_cache(THEME_CACHE):
+        admin_client.post(delete_link)
+
+
 def test_deleting_default_theme_sets_error_message(admin_client, default_theme):
     delete_link = reverse(
         "misago:admin:appearance:themes:delete", kwargs={"pk": default_theme.pk}
@@ -81,7 +122,7 @@ def test_default_theme_is_not_deleted(admin_client, default_theme):
     delete_link = reverse(
         "misago:admin:appearance:themes:delete", kwargs={"pk": default_theme.pk}
     )
-    response = admin_client.post(delete_link)
+    admin_client.post(delete_link)
     default_theme.refresh_from_db()
 
 
@@ -105,3 +146,31 @@ def test_active_theme_is_not_deleted(admin_client, theme):
     )
     admin_client.post(delete_link)
     theme.refresh_from_db()
+
+
+def test_deleting_theme_containing_active_child_theme_sets_error_message(
+    admin_client, theme, other_theme
+):
+    other_theme.move_to(theme)
+    other_theme.is_active = True
+    other_theme.save()
+
+    delete_link = reverse(
+        "misago:admin:appearance:themes:delete", kwargs={"pk": theme.pk}
+    )
+    response = admin_client.post(delete_link)
+    assert_has_error_message(response)
+
+
+def test_theme_containing_active_child_theme_is_not_deleted(
+    admin_client, theme, other_theme
+):
+    other_theme.move_to(theme)
+    other_theme.is_active = True
+    other_theme.save()
+
+    delete_link = reverse(
+        "misago:admin:appearance:themes:delete", kwargs={"pk": theme.pk}
+    )
+    admin_client.post(delete_link)
+    theme.refresh_from_db()

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

@@ -51,7 +51,7 @@ class EditTheme(ThemeAdmin, generic.ModelFormView):
             clear_theme_cache()
 
 
-class DeleteTheme(ThemeAdmin, generic.ModelFormView):
+class DeleteTheme(ThemeAdmin, generic.ButtonView):
     message_submit = _('Theme "%(name)s" has been deleted.')
 
     def check_permissions(self, request, target):
@@ -59,17 +59,19 @@ class DeleteTheme(ThemeAdmin, generic.ModelFormView):
             return gettext("Default theme can't be deleted.")
         if target.is_active:
             return gettext("Active theme can't be deleted.")
+        if target.get_descendants().filter(is_active=True).exists():
+            message = gettext(
+                'Theme "%(name)s" can\'t be deleted '
+                "because one of its child themes is set as active."
+            )
+            return message % {"name": target}
 
-    def real_dispatch(self, request, target):
-        if request.method == "POST" and target.is_leaf_node():
-            return self.delete_theme_without_children(request, target)
-
-        return super().real_dispatch(request, target)
+    def button_action(self, request, target):
+        for theme in reversed(target.get_descendants(include_self=True)):
+            theme.delete()
 
-    def delete_theme_without_children(self, request, target):
-        target.delete()
+        clear_theme_cache()
         messages.success(request, self.message_submit % {"name": target})
-        return redirect(self.root_link)
 
 
 class ActivateTheme(ThemeAdmin, generic.ButtonView):