views.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. from django.contrib import messages
  2. from django.db.models import ObjectDoesNotExist
  3. from django.shortcuts import redirect
  4. from django.utils.translation import gettext, gettext_lazy as _
  5. from ...admin.views import generic
  6. from ..cache import clear_theme_cache
  7. from ..models import Theme, Css
  8. from .css import move_css_down, move_css_up
  9. from .forms import CssEditorForm, CssLinkForm, ThemeForm, UploadCssForm, UploadMediaForm
  10. from .tasks import build_single_theme_css, build_theme_css, update_remote_css_size
  11. class ThemeAdmin(generic.AdminBaseMixin):
  12. root_link = "misago:admin:appearance:themes:index"
  13. model = Theme
  14. form = ThemeForm
  15. templates_dir = "misago/admin/themes"
  16. message_404 = _("Requested theme does not exist.")
  17. class ThemesList(ThemeAdmin, generic.ListView):
  18. pass
  19. class NewTheme(ThemeAdmin, generic.ModelFormView):
  20. message_submit = _('New theme "%(name)s" has been saved.')
  21. def initialize_form(self, form, request, _):
  22. if request.method == "POST":
  23. return form(request.POST, request.FILES)
  24. try:
  25. initial = {"parent": int(request.GET.get("parent"))}
  26. except (TypeError, ValueError):
  27. initial = {}
  28. return form(initial=initial)
  29. class EditTheme(ThemeAdmin, generic.ModelFormView):
  30. message_submit = _('Theme "%(name)s" has been updated.')
  31. def check_permissions(self, request, target):
  32. if target.is_default:
  33. return gettext("Default theme can't be edited.")
  34. def handle_form(self, form, request, target):
  35. super().handle_form(form, request, target)
  36. if "parent" in form.changed_data:
  37. clear_theme_cache()
  38. class DeleteTheme(ThemeAdmin, generic.ModelFormView):
  39. message_submit = _('Theme "%(name)s" has been deleted.')
  40. def check_permissions(self, request, target):
  41. if target.is_default:
  42. return gettext("Default theme can't be deleted.")
  43. if target.is_active:
  44. return gettext("Active theme can't be deleted.")
  45. def real_dispatch(self, request, target):
  46. if request.method == "POST" and target.is_leaf_node():
  47. return self.delete_theme_without_children(request, target)
  48. return super().real_dispatch(request, target)
  49. def delete_theme_without_children(self, request, target):
  50. target.delete()
  51. messages.success(request, self.message_submit % {"name": target})
  52. return redirect(self.root_link)
  53. class ActivateTheme(ThemeAdmin, generic.ButtonView):
  54. def button_action(self, request, target):
  55. set_theme_as_active(request, target)
  56. message = gettext('Active theme has been changed to "%(name)s".')
  57. messages.success(request, message % {"name": target})
  58. def set_theme_as_active(request, theme):
  59. Theme.objects.update(is_active=False)
  60. Theme.objects.filter(pk=theme.pk).update(is_active=True)
  61. clear_theme_cache()
  62. class ThemeAssetsAdmin(ThemeAdmin):
  63. def check_permissions(self, request, theme):
  64. if theme.is_default:
  65. return gettext("Default theme assets can't be edited.")
  66. def redirect_to_theme_assets(self, theme):
  67. return redirect("misago:admin:appearance:themes:assets", pk=theme.pk)
  68. class ThemeAssets(ThemeAssetsAdmin, generic.TargetedView):
  69. template = "assets/list.html"
  70. def real_dispatch(self, request, theme):
  71. return self.render(request, {"theme": theme})
  72. class ThemeAssetsActionAdmin(ThemeAssetsAdmin):
  73. def real_dispatch(self, request, theme):
  74. if request.method == "POST":
  75. self.action(request, theme)
  76. return self.redirect_to_theme_assets(theme)
  77. def action(self, request, theme):
  78. raise NotImplementedError(
  79. "action method must be implemented in inheriting class"
  80. )
  81. class UploadThemeAssets(ThemeAssetsActionAdmin, generic.TargetedView):
  82. message_partial_success = _(
  83. "Some css files could not have been added to the theme."
  84. )
  85. message_submit = None
  86. form = None
  87. def action(self, request, theme):
  88. form = self.form( # pylint: disable=not-callable
  89. request.POST, request.FILES, instance=theme
  90. )
  91. if not form.is_valid():
  92. if form.cleaned_data.get("assets"):
  93. messages.info(request, self.message_partial_success)
  94. for error in form.errors["assets"]:
  95. messages.error(request, error)
  96. if form.cleaned_data.get("assets"):
  97. form.save()
  98. build_theme_css.delay(theme.pk)
  99. messages.success(request, self.message_success)
  100. class UploadThemeCss(UploadThemeAssets):
  101. message_success = _("New CSS files have been added to the theme.")
  102. form = UploadCssForm
  103. class UploadThemeMedia(UploadThemeAssets):
  104. message_success = _("New media files have been added to the theme.")
  105. form = UploadMediaForm
  106. class DeleteThemeAssets(ThemeAssetsActionAdmin, generic.TargetedView):
  107. message_submit = None
  108. queryset_attr = None
  109. def action(self, request, theme):
  110. items = self.clean_items_list(request)
  111. if items:
  112. queryset = getattr(theme, self.queryset_attr)
  113. for item in items:
  114. self.delete_asset(queryset, item)
  115. messages.success(request, self.message_submit)
  116. def clean_items_list(self, request):
  117. try:
  118. return {int(i) for i in request.POST.getlist("item")[:20]}
  119. except (ValueError, TypeError):
  120. pass
  121. def delete_asset(self, queryset, item):
  122. try:
  123. queryset.get(pk=item).delete()
  124. except ObjectDoesNotExist:
  125. pass
  126. class DeleteThemeCss(DeleteThemeAssets):
  127. message_submit = _("Selected CSS files have been deleted.")
  128. queryset_attr = "css"
  129. def action(self, request, theme):
  130. super().action(request, theme)
  131. clear_theme_cache()
  132. class DeleteThemeMedia(DeleteThemeAssets):
  133. message_submit = _("Selected media have been deleted.")
  134. queryset_attr = "media"
  135. class ThemeCssAdmin(ThemeAssetsAdmin, generic.TargetedView):
  136. def wrapped_dispatch(self, request, pk, css_pk=None):
  137. theme = self.get_target_or_none(request, {"pk": pk})
  138. if not theme:
  139. messages.error(request, self.message_404)
  140. return redirect(self.root_link)
  141. error = self.check_permissions( # pylint: disable=assignment-from-no-return
  142. request, theme
  143. )
  144. if error:
  145. messages.error(request, error)
  146. return redirect(self.root_link)
  147. css = self.get_theme_css_or_none(theme, css_pk)
  148. if css_pk and not css:
  149. css_error = gettext("Requested CSS could not be found in the theme.")
  150. messages.error(request, css_error)
  151. return self.redirect_to_theme_assets(theme)
  152. return self.real_dispatch(request, theme, css)
  153. def get_theme_css_or_none(self, theme, css_pk):
  154. if not css_pk:
  155. return None
  156. try:
  157. return theme.css.select_for_update().get(pk=css_pk)
  158. except ObjectDoesNotExist:
  159. return None
  160. def real_dispatch(self, request, theme, css):
  161. raise NotImplementedError(
  162. "Admin views extending the ThemeCssAdmin"
  163. "should define real_dispatch(request, theme, css)"
  164. )
  165. class MoveThemeCssUp(ThemeCssAdmin):
  166. def real_dispatch(self, request, theme, css):
  167. if request.method == "POST" and move_css_up(theme, css):
  168. clear_theme_cache()
  169. messages.success(request, gettext('"%s" was moved up.') % css)
  170. return self.redirect_to_theme_assets(theme)
  171. class MoveThemeCssDown(ThemeCssAdmin):
  172. def real_dispatch(self, request, theme, css):
  173. if request.method == "POST" and move_css_down(theme, css):
  174. clear_theme_cache()
  175. messages.success(request, gettext('"%s" was moved down.') % css)
  176. return self.redirect_to_theme_assets(theme)
  177. class ThemeCssFormAdmin(ThemeCssAdmin, generic.ModelFormView):
  178. def real_dispatch(self, request, theme, css=None):
  179. form = self.initialize_form(self.form, request, theme, css)
  180. if request.method == "POST" and form.is_valid():
  181. response = self.handle_form( # pylint: disable=assignment-from-no-return
  182. form, request, theme, css
  183. )
  184. if response:
  185. return response
  186. if "stay" in request.POST:
  187. return self.redirect_to_edit_form(theme, form.instance)
  188. return self.redirect_to_theme_assets(theme)
  189. return self.render(request, {"form": form, "theme": theme, "target": css})
  190. def initialize_form(self, form, request, theme, css):
  191. raise NotImplementedError(
  192. "Admin views extending the ThemeCssFormAdmin "
  193. "should define the initialize_form(form, request, theme, css)"
  194. )
  195. def handle_form(self, form, request, theme, css):
  196. form.save()
  197. if css.source_needs_building:
  198. build_single_theme_css.delay(css.pk)
  199. else:
  200. clear_theme_cache()
  201. messages.success(request, self.message_submit % {"name": css.name})
  202. class NewThemeCss(ThemeCssFormAdmin):
  203. message_submit = _('New CSS "%(name)s" has been saved.')
  204. form = CssEditorForm
  205. template = "assets/css-editor-form.html"
  206. def get_theme_css_or_none(self, theme, _):
  207. return Css(theme=theme)
  208. def initialize_form(self, form, request, theme, css):
  209. if request.method == "POST":
  210. return form(request.POST, instance=css)
  211. return form(instance=css)
  212. def redirect_to_edit_form(self, theme, css):
  213. return redirect(
  214. "misago:admin:appearance:themes:edit-css-file", pk=theme.pk, css_pk=css.pk
  215. )
  216. class EditThemeCss(NewThemeCss):
  217. message_submit = _('CSS "%(name)s" has been updated.')
  218. def get_theme_css_or_none(self, theme, css_pk):
  219. try:
  220. return theme.css.get(pk=css_pk, url__isnull=True)
  221. except ObjectDoesNotExist:
  222. return None
  223. def initialize_form(self, form, request, theme, css):
  224. if request.method == "POST":
  225. return form(request.POST, instance=css)
  226. initial_data = {"source": css.source_file.read()}
  227. return form(instance=css, initial=initial_data)
  228. def handle_form(self, form, request, theme, css):
  229. if "source" in form.changed_data:
  230. form.save()
  231. if css.source_needs_building:
  232. build_single_theme_css.delay(css.pk)
  233. else:
  234. clear_theme_cache()
  235. messages.success(request, self.message_submit % {"name": css.name})
  236. else:
  237. message = gettext('No changes have been made to "%(css)s".')
  238. messages.info(request, message % {"name": css.name})
  239. class NewThemeCssLink(ThemeCssFormAdmin):
  240. message_submit = _('New CSS link "%(name)s" has been saved.')
  241. form = CssLinkForm
  242. template = "assets/css-link-form.html"
  243. def get_theme_css_or_none(self, theme, _):
  244. return Css(theme=theme)
  245. def initialize_form(self, form, request, theme, css):
  246. if request.method == "POST":
  247. return form(request.POST, instance=css)
  248. return form(instance=css)
  249. def handle_form(self, form, *args):
  250. super().handle_form(form, *args)
  251. if "url" in form.changed_data:
  252. update_remote_css_size.delay(form.instance.pk)
  253. clear_theme_cache()
  254. def redirect_to_edit_form(self, theme, css):
  255. return redirect("misago:admin:appearance:themes:new-css-link", pk=theme.pk)
  256. class EditThemeCssLink(NewThemeCssLink):
  257. message_submit = _('CSS link "%(name)s" has been updated.')
  258. def get_theme_css_or_none(self, theme, css_pk):
  259. try:
  260. return theme.css.get(pk=css_pk, url__isnull=False)
  261. except ObjectDoesNotExist:
  262. return None
  263. def redirect_to_edit_form(self, theme, css):
  264. return redirect(
  265. "misago:admin:appearance:themes:edit-css-link", pk=theme.pk, css_pk=css.pk
  266. )