views.py 11 KB

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