models.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. from datetime import datetime
  2. from django.conf import settings
  3. from django.core.validators import RegexValidator
  4. from django.db import models
  5. from modelcluster.fields import ParentalKey
  6. from wagtail.admin.panels import FieldPanel, InlinePanel
  7. from wagtail.fields import StreamField
  8. from wagtail.models import Orderable, Page
  9. from wagtail.search import index
  10. from wagtail_editable_help.models import HelpText
  11. from bakerydemo.base.blocks import BaseStreamBlock
  12. from bakerydemo.locations.choices import DAY_CHOICES
  13. class OperatingHours(models.Model):
  14. """
  15. A Django model to capture operating hours for a Location
  16. """
  17. day = models.CharField(max_length=4, choices=DAY_CHOICES, default="MON")
  18. opening_time = models.TimeField(blank=True, null=True)
  19. closing_time = models.TimeField(blank=True, null=True)
  20. closed = models.BooleanField(
  21. "Closed?",
  22. blank=True,
  23. help_text=HelpText(
  24. "Location page",
  25. "operating hours closed",
  26. default="Tick if location is closed on this day",
  27. ),
  28. )
  29. panels = [
  30. FieldPanel("day"),
  31. FieldPanel("opening_time"),
  32. FieldPanel("closing_time"),
  33. FieldPanel("closed"),
  34. ]
  35. class Meta:
  36. abstract = True
  37. def __str__(self):
  38. if self.opening_time:
  39. opening = self.opening_time.strftime("%H:%M")
  40. else:
  41. opening = "--"
  42. if self.closing_time:
  43. closed = self.closing_time.strftime("%H:%M")
  44. else:
  45. closed = "--"
  46. return "{}: {} - {} {}".format(self.day, opening, closed, settings.TIME_ZONE)
  47. class LocationOperatingHours(Orderable, OperatingHours):
  48. """
  49. A model creating a relationship between the OperatingHours and Location
  50. Note that unlike BlogPeopleRelationship we don't include a ForeignKey to
  51. OperatingHours as we don't need that relationship (e.g. any Location open
  52. a certain day of the week). The ParentalKey is the minimum required to
  53. relate the two objects to one another. We use the ParentalKey's related_
  54. name to access it from the LocationPage admin
  55. """
  56. location = ParentalKey(
  57. "LocationPage", related_name="hours_of_operation", on_delete=models.CASCADE
  58. )
  59. class LocationsIndexPage(Page):
  60. """
  61. A Page model that creates an index page (a listview)
  62. """
  63. introduction = models.TextField(
  64. help_text=HelpText(
  65. "Locations index page", "introduction", default="Text to describe the page"
  66. ),
  67. blank=True,
  68. )
  69. image = models.ForeignKey(
  70. "wagtailimages.Image",
  71. null=True,
  72. blank=True,
  73. on_delete=models.SET_NULL,
  74. related_name="+",
  75. help_text=HelpText(
  76. "Common",
  77. "hero image",
  78. default="Landscape mode only; horizontal width between 1000px and 3000px.",
  79. ),
  80. )
  81. # Only LocationPage objects can be added underneath this index page
  82. subpage_types = ["LocationPage"]
  83. # Allows children of this indexpage to be accessible via the indexpage
  84. # object on templates. We use this on the homepage to show featured
  85. # sections of the site and their child pages
  86. def children(self):
  87. return self.get_children().specific().live()
  88. # Overrides the context to list all child
  89. # items, that are live, by the date that they were published
  90. # https://docs.wagtail.org/en/stable/getting_started/tutorial.html#overriding-context
  91. def get_context(self, request):
  92. context = super(LocationsIndexPage, self).get_context(request)
  93. context["locations"] = (
  94. LocationPage.objects.descendant_of(self).live().order_by("title")
  95. )
  96. return context
  97. content_panels = Page.content_panels + [
  98. FieldPanel("introduction", classname="full"),
  99. FieldPanel("image"),
  100. ]
  101. class LocationPage(Page):
  102. """
  103. Detail for a specific bakery location.
  104. """
  105. introduction = models.TextField(
  106. help_text=HelpText(
  107. "Location page", "introduction", default="Text to describe the page"
  108. ),
  109. blank=True,
  110. )
  111. image = models.ForeignKey(
  112. "wagtailimages.Image",
  113. null=True,
  114. blank=True,
  115. on_delete=models.SET_NULL,
  116. related_name="+",
  117. help_text=HelpText(
  118. "Common",
  119. "hero image",
  120. default="Landscape mode only; horizontal width between 1000px and 3000px.",
  121. ),
  122. )
  123. body = StreamField(
  124. BaseStreamBlock(), verbose_name="Page body", blank=True, use_json_field=True
  125. )
  126. address = models.TextField()
  127. lat_long = models.CharField(
  128. max_length=36,
  129. help_text=HelpText(
  130. "Location page",
  131. "lat/long",
  132. default="Comma separated lat/long. (Ex. 64.144367, -21.939182) Right click Google Maps and select 'What's Here'",
  133. ),
  134. validators=[
  135. RegexValidator(
  136. regex=r"^(\-?\d+(\.\d+)?),\s*(\-?\d+(\.\d+)?)$",
  137. message="Lat Long must be a comma-separated numeric lat and long",
  138. code="invalid_lat_long",
  139. ),
  140. ],
  141. )
  142. # Search index configuration
  143. search_fields = Page.search_fields + [
  144. index.SearchField("address"),
  145. index.SearchField("body"),
  146. ]
  147. # Fields to show to the editor in the admin view
  148. content_panels = [
  149. FieldPanel("title", classname="full"),
  150. FieldPanel("introduction", classname="full"),
  151. FieldPanel("image"),
  152. FieldPanel("body"),
  153. FieldPanel("address", classname="full"),
  154. FieldPanel("lat_long"),
  155. InlinePanel("hours_of_operation", label="Hours of Operation"),
  156. ]
  157. def __str__(self):
  158. return self.title
  159. @property
  160. def operating_hours(self):
  161. hours = self.hours_of_operation.all()
  162. return hours
  163. # Determines if the location is currently open. It is timezone naive
  164. def is_open(self):
  165. now = datetime.now()
  166. current_time = now.time()
  167. current_day = now.strftime("%a").upper()
  168. try:
  169. self.operating_hours.get(
  170. day=current_day,
  171. opening_time__lte=current_time,
  172. closing_time__gte=current_time,
  173. )
  174. return True
  175. except LocationOperatingHours.DoesNotExist:
  176. return False
  177. # Makes additional context available to the template so that we can access
  178. # the latitude, longitude and map API key to render the map
  179. def get_context(self, request):
  180. context = super(LocationPage, self).get_context(request)
  181. context["lat"] = self.lat_long.split(",")[0]
  182. context["long"] = self.lat_long.split(",")[1]
  183. context["google_map_api_key"] = settings.GOOGLE_MAP_API_KEY
  184. return context
  185. # Can only be placed under a LocationsIndexPage object
  186. parent_page_types = ["LocationsIndexPage"]