views.py 12 KB

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