views.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. import mimetypes
  2. import os
  3. from datetime import datetime
  4. from django.http import (
  5. Http404,
  6. HttpResponse,
  7. HttpResponsePermanentRedirect,
  8. JsonResponse,
  9. )
  10. from django.contrib.auth.decorators import login_required, permission_required
  11. from django.contrib.contenttypes.models import ContentType
  12. from django.core.paginator import (
  13. Paginator,
  14. InvalidPage,
  15. EmptyPage,
  16. PageNotAnInteger,
  17. )
  18. from django.shortcuts import redirect, render
  19. from django.utils import timezone
  20. from django.utils.translation import ngettext, gettext_lazy as _
  21. from django.views.decorators.http import require_POST
  22. from icalendar import Calendar
  23. from wagtail.admin import messages
  24. from wagtail.models import Page, get_page_models
  25. from wagtail.search.backends import get_search_backend
  26. from wagtail.search.backends.database.mysql.mysql import MySQLSearchBackend
  27. from coderedcms import utils
  28. from coderedcms.forms import SearchForm
  29. from coderedcms.models import CoderedPage, LayoutSettings
  30. from coderedcms.importexport import (
  31. convert_csv_to_json,
  32. import_pages,
  33. ImportPagesFromCSVFileForm,
  34. )
  35. from coderedcms.settings import crx_settings
  36. from coderedcms.templatetags.coderedcms_tags import get_name_of_class
  37. def search(request):
  38. """
  39. Searches pages across the entire site.
  40. """
  41. search_form = SearchForm(request.GET)
  42. pagetypes = []
  43. results = None
  44. results_paginated = None
  45. if search_form.is_valid():
  46. search_query = search_form.cleaned_data["s"]
  47. search_model = search_form.cleaned_data["t"]
  48. # get all page models
  49. pagemodels = sorted(get_page_models(), key=get_name_of_class)
  50. # filter based on is search_filterable
  51. for model in pagemodels:
  52. if hasattr(model, "search_filterable") and model.search_filterable:
  53. pagetypes.append(model)
  54. results = Page.objects.live()
  55. if search_model:
  56. try:
  57. # If provided a model name, try to get it
  58. model = ContentType.objects.get(
  59. model=search_model
  60. ).model_class()
  61. # Workaround for Wagtail MySQL search bug.
  62. # See: https://github.com/wagtail/wagtail/issues/11273
  63. backend = get_search_backend()
  64. if type(backend) is MySQLSearchBackend:
  65. results = model.objects.live()
  66. else:
  67. results = results.type(model)
  68. except ContentType.DoesNotExist:
  69. # Maintain existing behavior of only returning objects if the page type is real
  70. results = None
  71. # get and paginate results
  72. if results:
  73. results = results.search(search_query)
  74. paginator = Paginator(
  75. results, LayoutSettings.for_request(request).search_num_results
  76. )
  77. page = request.GET.get("p", 1)
  78. try:
  79. results_paginated = paginator.page(page)
  80. except PageNotAnInteger:
  81. results_paginated = paginator.page(1)
  82. except EmptyPage:
  83. results_paginated = paginator.page(1)
  84. except InvalidPage:
  85. results_paginated = paginator.page(1)
  86. # Render template
  87. return render(
  88. request,
  89. "coderedcms/pages/search.html",
  90. {
  91. "request": request,
  92. "pagetypes": pagetypes,
  93. "form": search_form,
  94. "results": results,
  95. "results_paginated": results_paginated,
  96. },
  97. )
  98. @login_required
  99. def serve_protected_file(request, path):
  100. """
  101. Function that serves protected files uploaded from forms.
  102. """
  103. # Fully resolve all provided paths.
  104. mediapath = os.path.abspath(crx_settings.CRX_PROTECTED_MEDIA_ROOT)
  105. fullpath = os.path.abspath(os.path.join(mediapath, path))
  106. # Path must be a sub-path of the PROTECTED_MEDIA_ROOT, and exist.
  107. if fullpath.startswith(mediapath) and os.path.isfile(fullpath):
  108. mimetype, encoding = mimetypes.guess_type(fullpath)
  109. with open(fullpath, "rb") as f:
  110. response = HttpResponse(f.read(), content_type=mimetype)
  111. if encoding:
  112. response["Content-Encoding"] = encoding
  113. return response
  114. raise Http404()
  115. def favicon(request):
  116. icon = LayoutSettings.for_request(request).favicon
  117. if icon:
  118. # Try to convert to webp, otherwise pass original file format
  119. # This will happen mainly if the file is an SVG
  120. try:
  121. return HttpResponsePermanentRedirect(
  122. icon.get_rendition("fill-256x256|format-webp").url
  123. )
  124. except AttributeError:
  125. return HttpResponsePermanentRedirect(
  126. icon.get_rendition("fill-256x256").url
  127. )
  128. raise Http404()
  129. def robots(request):
  130. return render(request, "robots.txt", content_type="text/plain")
  131. @require_POST
  132. def event_generate_single_ical_for_event(request):
  133. # Parse input.
  134. try:
  135. event_pk = request.POST["event_pk"]
  136. except KeyError:
  137. return HttpResponse("event_pk required", status=400)
  138. try:
  139. dt_start_str = utils.fix_ical_datetime_format(
  140. request.POST["datetime_start"]
  141. )
  142. dt_end_str = utils.fix_ical_datetime_format(
  143. request.POST["datetime_end"]
  144. )
  145. dt_start = None
  146. dt_end = None
  147. if dt_start_str:
  148. dt_start = datetime.strptime(dt_start_str, "%Y-%m-%dT%H:%M:%S%z")
  149. if dt_end_str:
  150. dt_end = datetime.strptime(dt_end_str, "%Y-%m-%dT%H:%M:%S%z")
  151. except KeyError:
  152. return HttpResponse(
  153. "datetime_start and datetime_end required.",
  154. status=400,
  155. )
  156. except ValueError:
  157. return HttpResponse(
  158. "datetime_start and datetime_end must be valid datetimes.",
  159. status=400,
  160. )
  161. # Get the page.
  162. try:
  163. event = CoderedPage.objects.get(pk=event_pk).specific
  164. except (CoderedPage.DoesNotExist, ValueError):
  165. raise Http404("Event does not exist")
  166. # Generate the ical file.
  167. ical = Calendar()
  168. ical.add("prodid", "-//Wagtail CRX//")
  169. ical.add("version", "2.0")
  170. ical.add_component(
  171. event.create_single_ical(dt_start=dt_start, dt_end=dt_end)
  172. )
  173. response = HttpResponse(ical.to_ical(), content_type="text/calendar")
  174. response["Filename"] = "{0}.ics".format(event.slug)
  175. response["Content-Disposition"] = "attachment; filename={0}.ics".format(
  176. event.slug
  177. )
  178. return response
  179. @require_POST
  180. def event_generate_recurring_ical_for_event(request):
  181. # Parse input.
  182. try:
  183. event_pk = request.POST["event_pk"]
  184. except KeyError:
  185. return HttpResponse("event_pk required", status=400)
  186. # Get the page.
  187. try:
  188. event = CoderedPage.objects.get(pk=event_pk).specific
  189. except (CoderedPage.DoesNotExist, ValueError):
  190. raise Http404("Event does not exist")
  191. # Generate the ical file.
  192. ical = Calendar()
  193. ical.add("prodid", "-//Wagtail CRX//")
  194. ical.add("version", "2.0")
  195. for e in event.create_recurring_ical():
  196. ical.add_component(e)
  197. response = HttpResponse(ical.to_ical(), content_type="text/calendar")
  198. response["Filename"] = "{0}.ics".format(event.slug)
  199. response["Content-Disposition"] = "attachment; filename={0}.ics".format(
  200. event.slug
  201. )
  202. return response
  203. @require_POST
  204. def event_generate_ical_for_calendar(request):
  205. # Parse input.
  206. try:
  207. page_id = request.POST["page_id"]
  208. except KeyError:
  209. return HttpResponse("page_id required", status=400)
  210. # Get the page.
  211. try:
  212. page = CoderedPage.objects.get(pk=page_id).specific
  213. except (CoderedPage.DoesNotExist, ValueError):
  214. raise Http404("Page does not exist")
  215. # Generate the ical file.
  216. ical = Calendar()
  217. ical.add("prodid", "-//Wagtail CRX//")
  218. ical.add("version", "2.0")
  219. for event_page in page.get_index_children():
  220. for e in event_page.specific.create_recurring_ical():
  221. ical.add_component(e)
  222. response = HttpResponse(ical.to_ical(), content_type="text/calendar")
  223. response["Filename"] = "calendar.ics"
  224. response["Content-Disposition"] = "attachment; filename=calendar.ics"
  225. return response
  226. def event_get_calendar_events(request):
  227. """
  228. JSON list of events compatible with fullcalendar.js
  229. """
  230. # Parse input.
  231. try:
  232. page_id = request.GET["pid"]
  233. except KeyError:
  234. return HttpResponse("pid required", status=400)
  235. start = None
  236. end = None
  237. start_str = request.GET.get("start", None)
  238. end_str = request.GET.get("end", None)
  239. try:
  240. if start_str:
  241. start = timezone.make_aware(
  242. datetime.strptime(start_str[:10], "%Y-%m-%d"),
  243. )
  244. if end_str:
  245. end = timezone.make_aware(
  246. datetime.strptime(end_str[:10], "%Y-%m-%d"),
  247. )
  248. except ValueError:
  249. return HttpResponse(
  250. "start and end must be valid datetimes.", status=400
  251. )
  252. # Get the page.
  253. try:
  254. page = CoderedPage.objects.get(pk=page_id).specific
  255. except (CoderedPage.DoesNotExist, ValueError):
  256. raise Http404("Page does not exist")
  257. return JsonResponse(
  258. page.get_calendar_events(start=start, end=end), safe=False
  259. )
  260. @login_required
  261. @permission_required(
  262. "wagtailadmin.access_admin",
  263. login_url="wagtailadmin_login",
  264. )
  265. def import_index(request):
  266. """
  267. Landing page to replace wagtailimportexport.
  268. """
  269. return render(request, "wagtailimportexport/index.html")
  270. @login_required
  271. @permission_required(
  272. "wagtailadmin.access_admin",
  273. login_url="wagtailadmin_login",
  274. )
  275. def import_pages_from_csv_file(request):
  276. """
  277. Overwrite of the `import_pages` view from wagtailimportexport. By default, the `import_pages`
  278. view expects a json file to be uploaded. This view converts the uploaded csv into the json
  279. format that the importer expects.
  280. """
  281. if request.method == "POST":
  282. form = ImportPagesFromCSVFileForm(request.POST, request.FILES)
  283. if form.is_valid():
  284. import_data = convert_csv_to_json(
  285. form.cleaned_data["file"].read().decode("utf-8").splitlines(),
  286. form.cleaned_data["page_type"],
  287. )
  288. parent_page = form.cleaned_data["parent_page"]
  289. try:
  290. page_count = import_pages(import_data, parent_page)
  291. except LookupError as e:
  292. messages.error(
  293. request, _("Import failed: %(reason)s") % {"reason": e}
  294. )
  295. else:
  296. messages.success(
  297. request,
  298. ngettext(
  299. "%(count)s page imported.",
  300. "%(count)s pages imported.",
  301. page_count,
  302. )
  303. % {"count": page_count},
  304. )
  305. return redirect("wagtailadmin_explore", parent_page.pk)
  306. else:
  307. form = ImportPagesFromCSVFileForm()
  308. return render(
  309. request,
  310. "wagtailimportexport/import_from_csv.html",
  311. {
  312. "form": form,
  313. },
  314. )