views.py 13 KB

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