views.py 11 KB

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