advanced02.rst 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. Custom Page Types
  2. =================
  3. In many cases, the built-in page types will be exactly what you need. There are, however,
  4. several reasons why you may need a custom page type. This tutorial will show you an example
  5. for creating a custom page type.
  6. Let's say that we need to make a special product page for all of our cupcakes. While our real bakery
  7. may have over 100 different types, we will limit this example to a small handful but enough to show
  8. how this works.
  9. Before we begin, you should have a general understanding of `Django models <https://docs.djangoproject.com/en/stable/topics/db/models/>`_
  10. and some Python skills. You can still follow along for an introduction to these concepts even without this knowledge.
  11. We are also going to be unable to cover every potential use case or scenario in this tutorial, but we hope that it will springboard
  12. any ideas that you have for your own website.
  13. Prep work for custom pages
  14. --------------------------
  15. We need to plan our page ahead of time. What fields will our custom page need, and what will we need our page
  16. to do? In this example we are going to add a products page and a product landing page. As we add more products
  17. they will automatically be included on the landing page like articles. For this example, we have to decide what fields we need.
  18. **Product Page Fields:**
  19. * Name of the product (This will be the title of the page in this instance)
  20. * Photo of the product
  21. * Description of the product
  22. * Does this product require a prescription.
  23. **Product Landing Page Fields:**
  24. * Needs to be the parent page for the product pages
  25. Setting up the page models
  26. --------------------------
  27. Just like in Django or Wagtail, you will need to set up your page models in the ``models.py`` file of your
  28. project. Navigate to ``mysite\website\models.py`` in your code editor and open up the ``models.py`` file.
  29. You should already see a few page models in there from Wagtail CRX, as well as imports at the top from the
  30. frameworks that we are using.
  31. .. code-block:: python
  32. """
  33. Create or customize your page models here.
  34. """
  35. from modelcluster.fields import ParentalKey
  36. from coderedcms.forms import CoderedFormField
  37. from coderedcms.models import (
  38. CoderedArticlePage,
  39. CoderedArticleIndexPage,
  40. CoderedEmail,
  41. CoderedFormPage,
  42. CoderedWebPage,
  43. )
  44. class ArticlePage(CoderedArticlePage):
  45. """
  46. Article, suitable for news or blog content.
  47. """
  48. class Meta:
  49. verbose_name = "Article"
  50. ordering = ["-first_published_at"]
  51. # Only allow this page to be created beneath an ArticleIndexPage.
  52. parent_page_types = ["website.ArticleIndexPage"]
  53. template = "coderedcms/pages/article_page.html"
  54. search_template = "coderedcms/pages/article_page.search.html"
  55. ## OTHER CLASSES
  56. class WebPage(CoderedWebPage):
  57. """
  58. General use page with featureful streamfield and SEO attributes.
  59. """
  60. class Meta:
  61. verbose_name = "Web Page"
  62. template = "coderedcms/pages/web_page.html"
  63. Before we begin adding our fields for our new page models, we should add the page class, meta class,
  64. and template information for our pages.
  65. * We our extending the ``CoderedWebPage`` model which is why it is wrapped in parentheses after we name our page model.
  66. * We are indicating that Product pages are sub-pages of the Product Landing Page.
  67. * We are specifying the template files the page models use, these need to be created in the ``templates\website\pages`` folder.
  68. Add this code below the other page models:
  69. .. code:: python
  70. class ProductIndexPage(CoderedWebPage):
  71. """
  72. Landing page for Products
  73. """
  74. class Meta:
  75. verbose_name = "Product Landing Page"
  76. # Override to specify custom index ordering choice/default.
  77. index_query_pagemodel = 'website.ProductPage'
  78. # Only allow ProductPages beneath this page.
  79. subpage_types = ['website.ProductPage']
  80. template = 'website/pages/product_index_page.html'
  81. class ProductPage(CoderedWebPage):
  82. """
  83. Custom page for individual products
  84. """
  85. class Meta:
  86. verbose_name = "Product Page"
  87. # Only allow this page to be created beneath an ProductIndexPage.
  88. parent_page_types = ['website.ProductIndexPage']
  89. template = "website/pages/product_page.html"
  90. * Create the template pages ``product_page.html`` and ``product_index_page.html`` in the ``templates\website\pages`` folder.
  91. * At the top of these template pages, add these tags to have basic functioning template:
  92. .. code:: Django
  93. {% extends "coderedcms/pages/web_page.html" %}
  94. {% load wagtailcore_tags wagtailimages_tags coderedcms_tags %}
  95. Now we can turn our attention back to our page models, specifically the ProductPage.
  96. In this example the the name of the product will be the title of the Page.
  97. We need to add other fields to be be in alignment with the outline we looked at earlier.
  98. .. code:: python
  99. # At top of the file add these imports
  100. from django.db import models
  101. from wagtail.admin.edit_handlers import FieldPanel
  102. from wagtail.core.fields import RichTextField
  103. from wagtail.images import get_image_model_string
  104. from wagtail.images.edit_handlers import ImageChooserPanel
  105. # Update the product page with these fields
  106. class ProductPage(CoderedWebPage):
  107. """
  108. Custom page for individual products
  109. """
  110. class Meta:
  111. verbose_name = "Product Page"
  112. # Only allow this page to be created beneath an ProductIndexPage.
  113. parent_page_types = ['website.ProductIndexPage']
  114. template = "website/pages/product_page.html"
  115. # Product Page model fields
  116. description = RichTextField(
  117. verbose_name="Product Description",
  118. null=True,
  119. blank=True,
  120. default=""
  121. )
  122. photo = models.ForeignKey(
  123. get_image_model_string(),
  124. null=True,
  125. blank=True,
  126. on_delete=models.SET_NULL,
  127. related_name='+',
  128. verbose_name='Product Photo',
  129. )
  130. need_prescription = models.BooleanField(default=True)
  131. # Add custom fields to the body
  132. body_content_panels = CoderedWebPage.body_content_panels + [
  133. FieldPanel("description"),
  134. FieldPanel("photo"),
  135. FieldPanel("need_prescription"),
  136. ]
  137. **What's happening?**
  138. We had to add some imports at the top to be able to use these field types in our model.
  139. If we try to makemigrations/migrate without having these imported, it will show an error.
  140. Next, we added the necessary fields and their field types that specify functionality.
  141. * ``description`` is a RichTextField (essentially a text box) that allows formatting.
  142. * ``photo`` uses a ForeignKey to reference the image_mode. This allows an image to be uploaded via an ImageChooserPanel -- the popup we get when we want to add a photo in the CMS.
  143. * ``need_prescription`` is a Boolean value that tells us if the product requires a prescription.
  144. * ``body_content_panels`` is defining which page builder blocks the editing screen should show. (In this instance, it's using the wagtail-CRX standard blocks, plus the custom fields in the model.)
  145. These changes to the models have to be migrated in the the database. To do so:
  146. * You need to have an active virtual environment. (See :ref:`installation` notes for help)
  147. * (if running) Stop your server with ``control + c``
  148. * Run ``python manage.py makemigrations`` (The makemigrations command uses the models specify changes that are going to be made to the database)
  149. * Run ``python manage.py migrate`` (The migrate command makes those changes)
  150. It should migrate successfully. (If not, read what the error says and fix it. A typo can cause huge problems!)
  151. * Run the server with ``python manage.py runserver`` to see how it looks in your CMS admin.
  152. You should now see Product Landing Page as a page choice.
  153. .. figure:: img/A02/plp_as_child.jpeg
  154. :alt: product landing page in the page selector
  155. To be inline with our CRX-pharma design (from the getting started tutorial), we are going to add a **Product Landing Page** as a child of the "Our Products" page **Home > Our Products**.
  156. Hopefully, if you followed the tutorial this is a pretty basic operation. As a quick "refresher":
  157. * Click **Pages** in the side menu
  158. * Use the arrow and click on the "Our Product Page". ("If you don't have one, just use Home")
  159. * This opens the page management screen. Click **Add child page**
  160. * Choose page type **Product Landing Page**
  161. * Give it a title (Direct to Consumer Products)
  162. * (optional) Add a cover image.
  163. .. figure:: img/A02/plp_editor.jpeg
  164. :alt: product landing page editor
  165. * **Save** and **Publish**
  166. .. note::
  167. We still have to work on the templates for both Product Page and Product Landing Page. They are placeholders to prevent Django from throwing errors.
  168. Let's add a Product Page to the project:
  169. * Click **Add child page** on the the Product Landing Page we just published (titled "Direct to Consumer Products")
  170. .. figure:: img/A02/pp_editor.jpeg
  171. :alt: product page editor
  172. Look at the fields. The page editor has the standard "CoderedWebPage" fields (Title, Cover image, and Body) plus the ones we added to the model (Product Description, Product Photo, and Need prescription).
  173. * Make 3 or 4 Product pages.
  174. * Remember to **Save** and **Publish** each page.
  175. Building our custom page templates
  176. ----------------------------------
  177. Navigate to ``templates\website\pages\product_page.html``. This code was added earlier:
  178. .. code::
  179. {% extends "coderedcms/pages/web_page.html" %}
  180. {% load wagtailcore_tags wagtailimages_tags coderedcms_tags %}
  181. These page tags extend the wagtail-CRX page and gives us access to the data in store in the database for each page.
  182. Here is our whole template:
  183. **Our template code:**
  184. .. code:: Django
  185. {% extends "coderedcms/pages/web_page.html" %}
  186. {% load wagtailcore_tags wagtailimages_tags coderedcms_tags %}
  187. {% block content_pre_body %}
  188. {% if self.cover_image %}
  189. {% image page.cover_image fill-1600x900 format-webp as cover_image %}
  190. <div class="hero-bg mb-5" style="background-image:url({{cover_image.url}});">
  191. <div class="hero-fg">
  192. <div class="container text-center">
  193. <h1 class="text-white text-shadow">{{page.title}}</h1>
  194. </div>
  195. </div>
  196. </div>
  197. {% endif %}
  198. {% endblock %}
  199. {% block content_body %}
  200. <div class="block-row">
  201. <div class="container-fluid">
  202. <div class="row m-4">
  203. <div class="col-lg-6">
  204. {% if page.photo %}
  205. {% image page.photo fill-400x400 as product %}
  206. <div class="text-center">
  207. <img src="{{product.url}}" alt="photo of {{page.title}}">
  208. </div>
  209. {% endif %}
  210. </div>
  211. <div class="col-lg-6">
  212. <div class="py-lg-5">
  213. {% if self.cover_image %}
  214. {% else %}
  215. <h2 class="text-primary fw-bold fs-1 mb-2">{{page.title}}</h2>
  216. {% endif %}
  217. {% if page.description %}
  218. {{page.description|richtext}}
  219. {% endif %}
  220. {% if page.need_prescription %}
  221. <div class="fs-5"> Call your doctor to see if {{page.title}} is right for you!</div>
  222. {% else %}
  223. <div class="fs-5"> No Prescription Needed!</div>
  224. {% endif %}
  225. </div>
  226. </div>
  227. </div>
  228. </div>
  229. </div>
  230. {% endblock %}
  231. The ``{% block content_pre_body %}`` tag overrides part of the standard template. It uses conditional logic {% if %} to render the page title {{page.title}} in the header if the user
  232. adds a cover image. If they don't, the page.title renders in {% block content_body %}. There is a basic two column, with the image on the left and a description on the right. It also uses an
  233. {% if %} {% else %} statement with the {{page.need_prescription}} to render different text based on if the product needs a prescription.
  234. There is a lot to cover in Django templating. Check out the docs for more info `Django Templates <https://docs.djangoproject.com/en/4.1/topics/templates/>`_
  235. Here's the same template with and without a cover image:
  236. .. figure:: img/A02/pp_preview.jpeg
  237. :alt: product page preview with out cover image
  238. No cover image so the page title renders in the body.
  239. .. figure:: img/A02/pp_preview2.jpeg
  240. :alt: product page preview with cover image
  241. With the cover image and the title is centered with a bootstrap class "text-center" in the template.
  242. Building the Product Landing Page
  243. ---------------------------------
  244. While we could simply use the the default "Show Child Pages" option for the page, a list of links
  245. is rather boring. We also want the page to automatically update whenever we add a new product to save us lots of time
  246. and trouble. He is our template:
  247. .. code:: Django
  248. {% extends "coderedcms/pages/web_page.html" %}
  249. {% load wagtailcore_tags wagtailimages_tags coderedcms_tags %}
  250. {% block content_pre_body %}
  251. {% if self.cover_image %}
  252. {% image page.cover_image fill-1600x900 format-webp as cover_image %}
  253. <div class="hero-bg mb-5" style="background-image:url({{cover_image.url}});">
  254. <div class="hero-fg">
  255. <div class="d-flex justify-content-center">
  256. <h1 class="text-shadow text-white">{{page.title}}</h1>
  257. </div>
  258. </div>
  259. </div>
  260. {% else %}
  261. <div class="container">
  262. <h1>{{page.title}}</h1>
  263. </div>
  264. {% endif %}
  265. {% endblock %}
  266. {% block index_content %}
  267. <div class="container mb-5">
  268. <div class="row row-cols-2 row-cols-md-3 row-cols-xl-4">
  269. {% for product in page.get_children.specific %}
  270. <div class="col text-center">
  271. <div class="card h-100">
  272. {% if product.photo %}
  273. {% image product.photo fill-300x300 as product_photo %}
  274. <a href="{{product.url}}">
  275. <img class="card-img-top w-100" src="{{product_photo.url}}" alt="{{product.title}}">
  276. </a>
  277. {% endif %}
  278. <div class="card-body">
  279. <div class="card-text">
  280. <h5><a href="{{product.url}}">{{product.title}}</a></h5>
  281. {{product.description|richtext}}
  282. {% if page.need_prescription %}
  283. <p> Call your doctor to see if {{page.title}} is right for you!</p>
  284. {% else %}
  285. <p> No Prescription Needed!</p>
  286. {% endif %}
  287. </div>
  288. </div>
  289. </div>
  290. </div>
  291. {% endfor %}
  292. </div>
  293. </div>
  294. {% endblock %}
  295. **What's happening?**
  296. We are using a ``{% block index_content %}`` and a ``{% for product in page.get_children.specific %}`` loop that pulls
  297. in content from the child/sub-pages. Our new variable for the sub-pages is ``product``, so we reference the fields like so:
  298. ``{{product.title}}``. The different products are in cards in card-grid, size with different bootstrap breakpoints.
  299. This is what our published landing page looks like now:
  300. .. figure:: img/A02/plp_preview.jpeg
  301. :alt: product landing page preview
  302. Product Landing Page on a large screen.