views.py 10 KB

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