views.py 11 KB

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