models.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. import json
  2. from django.db import models
  3. """
  4. Create or customize your page models here.
  5. """
  6. from modelcluster.fields import ParentalKey
  7. from coderedcms.forms import CoderedFormField
  8. from coderedcms.models import (
  9. CoderedArticlePage,
  10. CoderedArticleIndexPage,
  11. CoderedEmail,
  12. CoderedFormPage,
  13. CoderedWebPage,
  14. )
  15. #from wagtail.models import PreviewableMixin
  16. from wagtail.fields import RichTextField, StreamField
  17. from wagtail.admin.panels import FieldPanel, InlinePanel
  18. from wagtail.contrib.routable_page.models import (
  19. RoutablePageMixin,
  20. path
  21. )
  22. from wagtailmedia.edit_handlers import MediaChooserPanel
  23. import wagtailmedia.blocks as wtm_blocks
  24. from wagtail.snippets.models import register_snippet
  25. import wagtail.blocks as wt_blocks
  26. import coderedcms.blocks as cr_blocks
  27. from django.utils.translation import gettext_lazy as _
  28. from wagtail.admin.widgets.chooser import BaseChooser
  29. #from wagtail.admin.widgets import AdminChooser
  30. from django import forms
  31. from wagtail.admin.staticfiles import versioned_static
  32. from generic_chooser.widgets import AdminChooser
  33. from generic_chooser.views import ModelChooserViewSet
  34. from django.contrib.admin.utils import quote
  35. from django.urls import reverse
  36. from wagtail.search import index
  37. class ArticlePage(CoderedArticlePage):
  38. """
  39. Article, suitable for news or blog content.
  40. """
  41. class Meta:
  42. verbose_name = "Article"
  43. ordering = ["-first_published_at"]
  44. # Only allow this page to be created beneath an ArticleIndexPage.
  45. parent_page_types = ["website.ArticleIndexPage"]
  46. template = "coderedcms/pages/article_page.html"
  47. search_template = "coderedcms/pages/article_page.search.html"
  48. class ArticleIndexPage(CoderedArticleIndexPage):
  49. """
  50. Shows a list of article sub-pages.
  51. """
  52. class Meta:
  53. verbose_name = "Article Landing Page"
  54. # Override to specify custom index ordering choice/default.
  55. index_query_pagemodel = "website.ArticlePage"
  56. # Only allow ArticlePages beneath this page.
  57. subpage_types = ["website.ArticlePage"]
  58. template = "coderedcms/pages/article_index_page.html"
  59. class FormPage(CoderedFormPage):
  60. """
  61. A page with an html <form>.
  62. """
  63. class Meta:
  64. verbose_name = "Form"
  65. template = "coderedcms/pages/form_page.html"
  66. class FormPageField(CoderedFormField):
  67. """
  68. A field that links to a FormPage.
  69. """
  70. class Meta:
  71. ordering = ["sort_order"]
  72. page = ParentalKey("FormPage", related_name="form_fields")
  73. class FormConfirmEmail(CoderedEmail):
  74. """
  75. Sends a confirmation email after submitting a FormPage.
  76. """
  77. page = ParentalKey("FormPage", related_name="confirmation_emails")
  78. class WebPage(CoderedWebPage):
  79. """
  80. General use page with featureful streamfield and SEO attributes.
  81. """
  82. class Meta:
  83. verbose_name = "Web Page"
  84. template = "coderedcms/pages/web_page.html"
  85. class Query (models.Model):
  86. id = models.AutoField(primary_key=True, db_column='rowid')
  87. created_at = models.DateTimeField()
  88. twitter_user_id = models.CharField(db_column='user_id', max_length=31)
  89. last_accessed_at = models.DateTimeField()
  90. next_token = models.CharField(max_length=127)
  91. query_type = models.CharField(max_length=63)
  92. auth_user_id = models.CharField(max_length=31)
  93. class Meta:
  94. managed = False
  95. db_table = 'query'
  96. class User (models.Model):
  97. id = models.AutoField(primary_key=True, db_column='rowid')
  98. user_id = models.CharField(db_column='id', max_length=31)
  99. accessed_at = models.DateTimeField()
  100. query = models.ForeignKey(
  101. Query,
  102. on_delete = models.CASCADE,
  103. blank = False,
  104. null = False
  105. )
  106. data = models.BinaryField()
  107. class Meta:
  108. managed = False
  109. db_table = 'user'
  110. class Tweet (models.Model, index.Indexed):
  111. id = models.AutoField(primary_key=True, db_column='rowid')
  112. tweet_id = models.CharField(db_column='id', max_length=31)
  113. accessed_at = models.DateTimeField()
  114. created_at = models.DateTimeField()
  115. query = models.ForeignKey(
  116. Query,
  117. on_delete = models.CASCADE,
  118. blank = False,
  119. null = False
  120. )
  121. data = models.BinaryField()
  122. search_fields = [
  123. index.SearchField('tweet_id'),
  124. index.SearchField('data'),
  125. index.FilterField('created_at'),
  126. ]
  127. class Meta:
  128. managed = False
  129. db_table = 'tweet'
  130. class Medium (models.Model):
  131. id = models.AutoField(primary_key=True, db_column='rowid')
  132. media_key = models.CharField(db_column='id', max_length=31)
  133. accessed_at = models.DateTimeField()
  134. query = models.ForeignKey(
  135. Query,
  136. on_delete = models.CASCADE,
  137. blank = False,
  138. null = False
  139. )
  140. data = models.BinaryField()
  141. class Meta:
  142. managed = False
  143. db_table = 'medium'
  144. class TwitterDatabaseRouter (object):
  145. """
  146. Denies writes to Tweet model,
  147. And routes reads to our custom DB defined in DATABASES
  148. """
  149. def db_for_write(self, model, **hints):
  150. if model in (Query, User, Tweet, Medium):
  151. raise Exception("Twitter model is read only.")
  152. def db_for_read(self, model, **hints):
  153. if model in (Query, User, Tweet, Medium):
  154. return 'tweets'
  155. def allow_relation (self, obj1, obj2, **hints):
  156. if isinstance(obj2, (Query, User, Tweet, Medium)):
  157. print('relation to a Tweet object')
  158. return True
  159. return True
  160. @register_snippet
  161. class TweetQuery (models.Model):
  162. """
  163. To reuse Block logic we could create a StreamField that only allows 1 TweetQueryBlock.
  164. """
  165. query_id = models.IntegerField(null=False, blank=False)
  166. panels = [
  167. FieldPanel("query_id"),
  168. ]
  169. @property
  170. def tweets (self):
  171. query_tweets = Tweet.objects.filter(query_id = self.query_id)
  172. tweets = []
  173. for query_tweet in query_tweets:
  174. tweet = json.loads(query_tweet.data)
  175. tweets.append(tweet)
  176. return tweets
  177. def __str__(self):
  178. return self.query_id
  179. class TweetQueryBlockValue(wt_blocks.StructValue):
  180. """
  181. Exposes the collection of Tweets for a TweetQueryBlock
  182. For use within templates using the .tweets attribute of the block instance.
  183. """
  184. @property
  185. def tweets (self):
  186. query_id2 = int(self.get('query_id2'))
  187. print(f'get tweets: {query_id2}')
  188. query_tweets = Tweet.objects.filter(query_id = query_id2)
  189. tweets = []
  190. for query_tweet in query_tweets:
  191. tweet = json.loads(query_tweet.data)
  192. tweets.append(tweet)
  193. return tweets
  194. class TweetQueryBlock (wt_blocks.StructBlock):
  195. """
  196. Allows for selection of a Tweet query
  197. And exposes the list of Tweets to the template.
  198. """
  199. query_id2 = wt_blocks.IntegerBlock()
  200. # IGNORE - failed attempt
  201. query_id = models.IntegerField(null=False, blank=False)
  202. def get_context(self, value, parent_context=None):
  203. """
  204. Another approach we can use instead of value_class
  205. """
  206. print("TweetQueryBlock.get_context")
  207. context = super().get_context(value, parent_context=parent_context)
  208. return context
  209. class Meta:
  210. value_class = TweetQueryBlockValue
  211. template = 'blocks/tweet-query-block.html'
  212. # https://github.com/wagtail/wagtail-generic-chooser/issues/10
  213. class TweetChooserViewSet (ModelChooserViewSet):
  214. icon = 'user'
  215. model = Tweet
  216. page_title = _("Choose a tweet")
  217. per_page = 10
  218. order_by = 'created_at'
  219. #fields = ['id', 'created_at', 'tweet_id'] # , 'data'
  220. #is_searchable = True
  221. title_field_name = 'tweet_id'
  222. class TweetChooser(AdminChooser ):
  223. choose_one_text = _('Choose a tweet')
  224. choose_another_text = _('Choose another tweet')
  225. link_to_chosen_text = _('Edit this tweet')
  226. model = Tweet
  227. choose_modal_url_name = 'tweet_chooser:choose'
  228. icon = 'user'
  229. #is_searchable = True
  230. TWEET_STREAMBLOCKS = cr_blocks.CONTENT_STREAMBLOCKS + [
  231. ('tweet_query_block', TweetQueryBlock()),
  232. ('video_block', wtm_blocks.VideoChooserBlock()),
  233. ('audio_block', wtm_blocks.AudioChooserBlock()),
  234. ]
  235. class ExternalProfilePage (RoutablePageMixin, CoderedWebPage):
  236. # Routable pages can have fields like any other - here we would
  237. # render the intro text on a template with {{ page.intro|richtext }}
  238. intro = RichTextField()
  239. intro2 = StreamField(TWEET_STREAMBLOCKS, null=True, blank=True, use_json_field=True)
  240. featured_media = models.ForeignKey(
  241. "wagtailmedia.Media",
  242. null=True,
  243. blank=True,
  244. on_delete=models.SET_NULL,
  245. related_name="+",
  246. )
  247. featured_tweet = models.ForeignKey(
  248. Tweet,
  249. null=True,
  250. blank=True,
  251. on_delete=models.SET_NULL,
  252. related_name="+",
  253. )
  254. #@path('')
  255. #def root (self, request):
  256. # return self.render(request)
  257. # return TemplateResponse(request, self.get_template(), self.get_context())
  258. def get_preview_context (self, request, mode_name):
  259. context = super().get_preview_context(request, mode_name)
  260. context.update({
  261. 'tweets': [{'text': 'PREVIEW', 'id': 'X'}]
  262. })
  263. return context
  264. def render (self, request, *args, template=None, context_overrides=None, **kwargs):
  265. """
  266. This isn't automatically done by Wagtail. I think it's a regression.
  267. This may result in get_contxt being called twice, since it's called from
  268. the super's version of get_preview_context.
  269. """
  270. print('CUSTOM RENDER')
  271. context = {}
  272. if hasattr(request, 'is_preview') and request.is_preview:
  273. print('render is_preview')
  274. context.update(self.get_preview_context(request, ''))
  275. # HACK... unsure this is correct... live/preview or live/draft
  276. setattr(self, 'preview', True)
  277. if context_overrides:
  278. context.update(context_overrides)
  279. return super().render(request, *args, template=template, context_overrides=context, **kwargs)
  280. @path('')
  281. def main(self, request):
  282. """
  283. The super version of this doesn't use its own render method, so we don'tell
  284. get preview context if we're in preview mode.
  285. """
  286. return self.render(request)
  287. @path('<int:user_id>/')
  288. @path('me/')
  289. def events_for_year(self, request, user_id=None):
  290. return self.render(request, context_overrides={
  291. 'user_id': user_id,
  292. 'is_me': user_id == None,
  293. 'tweets': self.latest_tweets
  294. })
  295. @property
  296. def latest_tweets (self):
  297. query_tweets = Tweet.objects.filter(query_id = 400)
  298. tweets = []
  299. for query_tweet in query_tweets:
  300. tweet = json.loads(query_tweet.data)
  301. tweets.append(tweet)
  302. return tweets
  303. template = "externalprofilepage/profile.html"
  304. content_panels = CoderedWebPage.content_panels + [
  305. FieldPanel('intro') ,
  306. FieldPanel('intro2') ,
  307. MediaChooserPanel("featured_media"),
  308. FieldPanel("featured_tweet", widget=TweetChooser()),
  309. #FieldPanel("featured_tweet"),
  310. ]