2
0

models.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. from django import forms
  2. from django.contrib.contenttypes.fields import GenericRelation
  3. from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
  4. from django.db import models
  5. from modelcluster.fields import ParentalManyToManyField
  6. from wagtail.admin.panels import FieldPanel, MultiFieldPanel
  7. from wagtail.api import APIField
  8. from wagtail.fields import StreamField
  9. from wagtail.models import DraftStateMixin, Page, RevisionMixin
  10. from wagtail.search import index
  11. from bakerydemo.base.blocks import BaseStreamBlock
  12. from bakerydemo.headless import CustomHeadlessMixin
  13. class Country(models.Model):
  14. """
  15. A Django model to store set of countries of origin.
  16. It is made accessible in the Wagtail admin interface through the CountrySnippetViewSet
  17. class in wagtail_hooks.py. This allows us to customize the admin interface for this snippet.
  18. In the BreadPage model you'll see we use a ForeignKey to create the relationship between
  19. Country and BreadPage. This allows a single relationship (e.g only one
  20. Country can be added) that is one-way (e.g. Country will have no way to
  21. access related BreadPage objects).
  22. """
  23. title = models.CharField(max_length=100)
  24. api_fields = [
  25. APIField("title"),
  26. ]
  27. def __str__(self):
  28. return self.title
  29. class Meta:
  30. verbose_name = "country of origin"
  31. verbose_name_plural = "countries of origin"
  32. class BreadIngredient(DraftStateMixin, RevisionMixin, models.Model):
  33. """
  34. A Django model to store a single ingredient.
  35. It is made accessible in the Wagtail admin interface through the BreadIngredientSnippetViewSet
  36. class in wagtail_hooks.py. This allows us to customize the admin interface for this snippet.
  37. We use a piece of functionality available to Wagtail called the ParentalManyToManyField on the BreadPage
  38. model to display this. The Wagtail Docs give a slightly more detailed example
  39. https://docs.wagtail.org/en/stable/getting_started/tutorial.html#categories
  40. """
  41. name = models.CharField(max_length=255)
  42. revisions = GenericRelation(
  43. "wagtailcore.Revision",
  44. content_type_field="base_content_type",
  45. object_id_field="object_id",
  46. related_query_name="bread_ingredient",
  47. for_concrete_model=False,
  48. )
  49. panels = [
  50. FieldPanel("name"),
  51. ]
  52. api_fields = [
  53. APIField("name"),
  54. ]
  55. def __str__(self):
  56. return self.name
  57. class Meta:
  58. verbose_name = "bread ingredient"
  59. verbose_name_plural = "bread ingredients"
  60. class BreadType(RevisionMixin, models.Model):
  61. """
  62. A Django model to define the bread type
  63. It is made accessible in the Wagtail admin interface through the BreadTypeSnippetViewSet
  64. class in wagtail_hooks.py. This allows us to customize the admin interface for this snippet.
  65. In the BreadPage model you'll see we use a ForeignKey
  66. to create the relationship between BreadType and BreadPage. This allows a
  67. single relationship (e.g only one BreadType can be added) that is one-way
  68. (e.g. BreadType will have no way to access related BreadPage objects)
  69. """
  70. title = models.CharField(max_length=255)
  71. revisions = GenericRelation(
  72. "wagtailcore.Revision",
  73. content_type_field="base_content_type",
  74. object_id_field="object_id",
  75. related_query_name="bread_type",
  76. for_concrete_model=False,
  77. )
  78. panels = [
  79. FieldPanel("title"),
  80. ]
  81. api_fields = [
  82. APIField("title"),
  83. ]
  84. def __str__(self):
  85. return self.title
  86. class Meta:
  87. verbose_name = "bread type"
  88. verbose_name_plural = "bread types"
  89. class BreadPage(CustomHeadlessMixin, Page):
  90. """
  91. Detail view for a specific bread
  92. """
  93. introduction = models.TextField(help_text="Text to describe the page", blank=True)
  94. image = models.ForeignKey(
  95. "wagtailimages.Image",
  96. null=True,
  97. blank=True,
  98. on_delete=models.SET_NULL,
  99. related_name="+",
  100. help_text="Landscape mode only; horizontal width between 1000px and 3000px.",
  101. )
  102. body = StreamField(
  103. BaseStreamBlock(), verbose_name="Page body", blank=True, use_json_field=True
  104. )
  105. origin = models.ForeignKey(
  106. Country,
  107. on_delete=models.SET_NULL,
  108. null=True,
  109. blank=True,
  110. )
  111. # We include related_name='+' to avoid name collisions on relationships.
  112. # e.g. there are two FooPage models in two different apps,
  113. # and they both have a FK to bread_type, they'll both try to create a
  114. # relationship called `foopage_objects` that will throw a valueError on
  115. # collision.
  116. bread_type = models.ForeignKey(
  117. "breads.BreadType",
  118. null=True,
  119. blank=True,
  120. on_delete=models.SET_NULL,
  121. related_name="+",
  122. )
  123. ingredients = ParentalManyToManyField("BreadIngredient", blank=True)
  124. content_panels = Page.content_panels + [
  125. FieldPanel("introduction"),
  126. FieldPanel("image"),
  127. FieldPanel("body"),
  128. FieldPanel("origin"),
  129. FieldPanel("bread_type"),
  130. MultiFieldPanel(
  131. [
  132. FieldPanel(
  133. "ingredients",
  134. widget=forms.CheckboxSelectMultiple,
  135. ),
  136. ],
  137. heading="Additional Metadata",
  138. classname="collapsed",
  139. ),
  140. ]
  141. search_fields = Page.search_fields + [
  142. index.SearchField("body"),
  143. ]
  144. parent_page_types = ["BreadsIndexPage"]
  145. api_fields = [
  146. APIField("introduction"),
  147. APIField("image"),
  148. APIField("body"),
  149. APIField("origin"),
  150. APIField("bread_type"),
  151. APIField("ingredients"),
  152. ]
  153. class BreadsIndexPage(CustomHeadlessMixin, Page):
  154. """
  155. Index page for breads.
  156. This is more complex than other index pages on the bakery demo site as we've
  157. included pagination. We've separated the different aspects of the index page
  158. to be discrete functions to make it easier to follow
  159. """
  160. introduction = models.TextField(help_text="Text to describe the page", blank=True)
  161. image = models.ForeignKey(
  162. "wagtailimages.Image",
  163. null=True,
  164. blank=True,
  165. on_delete=models.SET_NULL,
  166. related_name="+",
  167. help_text="Landscape mode only; horizontal width between 1000px and 3000px.",
  168. )
  169. content_panels = Page.content_panels + [
  170. FieldPanel("introduction"),
  171. FieldPanel("image"),
  172. ]
  173. # Can only have BreadPage children
  174. subpage_types = ["BreadPage"]
  175. api_fields = [
  176. APIField("introduction"),
  177. APIField("image"),
  178. ]
  179. # Returns a queryset of BreadPage objects that are live, that are direct
  180. # descendants of this index page with most recent first
  181. def get_breads(self):
  182. return (
  183. BreadPage.objects.live().descendant_of(self).order_by("-first_published_at")
  184. )
  185. # Allows child objects (e.g. BreadPage objects) to be accessible via the
  186. # template. We use this on the HomePage to display child items of featured
  187. # content
  188. def children(self):
  189. return self.get_children().specific().live()
  190. # Pagination for the index page. We use the `django.core.paginator` as any
  191. # standard Django app would, but the difference here being we have it as a
  192. # method on the model rather than within a view function
  193. def paginate(self, request, *args):
  194. page = request.GET.get("page")
  195. paginator = Paginator(self.get_breads(), 12)
  196. try:
  197. pages = paginator.page(page)
  198. except PageNotAnInteger:
  199. pages = paginator.page(1)
  200. except EmptyPage:
  201. pages = paginator.page(paginator.num_pages)
  202. return pages
  203. # Returns the above to the get_context method that is used to populate the
  204. # template
  205. def get_context(self, request):
  206. context = super(BreadsIndexPage, self).get_context(request)
  207. # BreadPage objects (get_breads) are passed through pagination
  208. breads = self.paginate(request, self.get_breads())
  209. context["breads"] = breads
  210. return context