/blogmaker/blog/models.py

http://blogmaker.googlecode.com/ · Python · 349 lines · 222 code · 76 blank · 51 comment · 31 complexity · c22e7a807307b11858a05a8fe7d98321 MD5 · raw file

  1. ''' Blog model classes
  2. Copyright (c) 2006-2007, PreFab Software Inc.
  3. Copyright (c) 2006, Andrew Gwozdziewycz <apgwoz@gmail.com>
  4. All rights reserved.
  5. '''
  6. import datetime, logging, re, socket, threading, xmlrpclib
  7. from django.db import models
  8. from django.contrib.auth.models import User
  9. from django.contrib.sites.models import Site
  10. from django.contrib.markup.templatetags.markup import markdown
  11. from django.conf import settings
  12. from django.utils.html import strip_tags
  13. from blogmaker.util import expand_shortcuts, strip_domain_and_rest, unescape
  14. import blogmaker.blog.trackback_client as tc
  15. ################################### Ping URL ###########################################
  16. # Thanks to http://www.imalm.com/blog/2007/feb/10/using-django-signals-ping-sites-update/
  17. # for this code!
  18. class PingUrl(models.Model):
  19. ping_url = models.URLField(verify_exists=False)
  20. blog_url = models.URLField(verify_exists=False)
  21. blog_name = models.CharField(maxlength=200)
  22. class Admin:
  23. list_display = ('ping_url','blog_url', 'blog_name')
  24. @staticmethod
  25. def pingAll():
  26. def _pingAll():
  27. oldTimeout = socket.getdefaulttimeout()
  28. socket.setdefaulttimeout(30)
  29. for pingObj in PingUrl.objects.all():
  30. try:
  31. s = xmlrpclib.Server(pingObj.ping_url)
  32. reply = s.weblogUpdates.ping(pingObj.blog_name, pingObj.blog_url)
  33. except:
  34. logging.error('Ping failed for ' + pingObj.ping_url, exc_info=True)
  35. socket.setdefaulttimeout(oldTimeout)
  36. threading.Thread(target=_pingAll).start()
  37. ################################### Tag ###########################################
  38. class Tag(models.Model):
  39. tag = models.CharField(maxlength=255, core=True, unique=True)
  40. slug = models.SlugField(unique=True, prepopulate_from=('tag',), maxlength=120)
  41. class Admin:
  42. list_display = ('tag', 'slug')
  43. search_fields = 'tag slug'.split()
  44. def __str__(self):
  45. return self.tag
  46. def get_absolute_url(self):
  47. return '%stag/%s/' % (settings.BLOG_ROOT, self.slug)
  48. ################################### Entry ###########################################
  49. class EntryManager(models.Manager):
  50. def current_active(self):
  51. ''' A QuerySet of all active entries before the current date/time '''
  52. # Note: The value of 'now' is cached in the returned QuerySet
  53. # so don't cache and reuse the QuerySet itself
  54. now = datetime.datetime.now()
  55. return self.filter(active=True).exclude(pub_date__gt=now)
  56. def index_blog(self):
  57. ''' The most recent entry object with truncated body '''
  58. blog_entry = self.current_active()[0]
  59. body = blog_entry.body
  60. body = markdown(body)
  61. body = expand_shortcuts(body)
  62. body = body.replace('<span class="info_icon">i</span>', '')
  63. body = re.split('<(hr|table|blockquote|div)', body)[0]
  64. body = strip_tags(body)
  65. body = body.split()
  66. body = ' '.join(body[:15])
  67. body = "%s ..." % body.strip()
  68. return blog_entry.headline, body
  69. class Entry(models.Model):
  70. objects = EntryManager()
  71. pub_date = models.DateTimeField(db_index=True)
  72. slug = models.SlugField(unique_for_date='pub_date', prepopulate_from=('headline',),
  73. maxlength=120, help_text="Slug is the same as headline, but with dashes for spaces")
  74. active = models.BooleanField(default=True, help_text="Is post viewable on site?")
  75. headline = models.CharField(maxlength=255)
  76. summary = models.CharField(maxlength=255, null=True, blank=True, help_text="Leave this field blank")
  77. image = models.ImageField(upload_to='photos/%Y/%m/%d', null=True, blank=True)
  78. copyright = models.CharField(maxlength=255, null=True, blank=True, help_text="Choose an image file and input any attribution info")
  79. body = models.TextField()
  80. user = models.ForeignKey(User, default=settings.DEFAULT_BLOG_USER, related_name="user_entries")
  81. tags = models.ManyToManyField(Tag, blank=True, filter_interface=models.HORIZONTAL)
  82. related_entries = models.ManyToManyField("self", blank=True, symmetrical=True, filter_interface=models.HORIZONTAL)
  83. externalId = models.IntegerField(blank=True, null=True)
  84. class Admin:
  85. list_display = ('slug', 'headline', 'pub_date', 'image', 'active')
  86. search_fields = 'headline body copyright'.split()
  87. fields = (
  88. ('Essentials', {
  89. 'fields': ('headline',
  90. 'slug',
  91. 'pub_date',
  92. 'active',
  93. ('image', 'copyright'),
  94. 'body',
  95. 'tags',
  96. 'related_entries',
  97. 'user',
  98. ),
  99. }),
  100. ('Optional', {
  101. 'classes': 'collapse',
  102. 'fields': ('summary', 'externalId'
  103. ),
  104. }),
  105. )
  106. js = [(settings.BLOG_MEDIA_PREFIX + 'js/jquery.js'), (settings.BLOG_MEDIA_PREFIX + 'js/entry_change_form.js')]
  107. save_on_top = True
  108. class Meta:
  109. get_latest_by = "pub_date"
  110. ordering = ['-pub_date']
  111. verbose_name_plural = "entries"
  112. def __str__(self):
  113. return self.headline
  114. def get_absolute_url(self):
  115. return '%s%s/%s/' % (settings.BLOG_ROOT, self.pub_date.strftime('%Y/%m/%d'), self.slug)
  116. def get_tags(self):
  117. return self.tags.all()
  118. def get_related_entries(self):
  119. return self.related_entries.current_active()
  120. anchorRe = re.compile(r'''<a [^>]*>''', re.DOTALL)
  121. hrefRe = re.compile(r'''href=['"](http.*?)['"]''')
  122. markdownAnchorRe = re.compile(r'\[[^\]]+\]\((http[^\s)]+)', re.DOTALL)
  123. @property
  124. def links(self):
  125. ''' Return a list of the target href of all links in this entry '''
  126. links = set()
  127. # <a> tags
  128. for anchor in self.anchorRe.findall(self.body):
  129. m = self.hrefRe.search(anchor)
  130. if m:
  131. links.add(m.group(1))
  132. # Markdown links
  133. links.update(anchor for anchor in self.markdownAnchorRe.findall(self.body))
  134. return links
  135. @property
  136. def excerpt(self):
  137. ''' A brief excerpt for trackbacks '''
  138. return unescape(strip_tags(expand_shortcuts(markdown(self.body))).decode('utf-8'))[:200]
  139. def save(self):
  140. models.Model.save(self)
  141. self.update_trackbacks()
  142. if (self.active):
  143. PingUrl.pingAll()
  144. # Don't send trackbacks to any of these sites
  145. _dontTrackbackSites = set('google.com technorati.com alexa.com quantcast.com'.split())
  146. def update_trackbacks(self):
  147. ''' Update the current list of trackbacks to match the current links '''
  148. trackbacksToDelete = dict((tb.link, tb) for tb in self.trackbacks.all())
  149. for link in self.links:
  150. if link in trackbacksToDelete:
  151. # Link has a trackback, keep it
  152. del trackbacksToDelete[link]
  153. else:
  154. host, rest = strip_domain_and_rest(link)
  155. # Skip known don't trackback sites
  156. if host in self._dontTrackbackSites:
  157. continue
  158. # Skip links with no path info or just /blog/
  159. if rest in ('/', '/blog'):
  160. continue
  161. # Create a new trackback for this link
  162. self.trackbacks.create(link=link)
  163. # Delete trackbacks that no longer have links unless they have some status
  164. for tb in trackbacksToDelete.values():
  165. if tb.status == 'NotYet':
  166. tb.delete()
  167. @staticmethod
  168. def entryCreateOrUpdate(pub_date, headline, body, user, externalId, active=True):
  169. lowerHeadline = headline.lower()
  170. slug = re.sub(' & ', '-and-', lowerHeadline)
  171. slug = re.sub(r'[\s/\?]+', '-', slug)
  172. slug = re.sub(r'[\:\$\#\@\*\(\)\!\.]+', '', slug)
  173. slug = re.sub(r'[^\w.~-]', '', slug)
  174. try:
  175. entry = Entry.objects.get(externalId=externalId)
  176. except:
  177. entry = Entry(externalId=externalId)
  178. entry.slug = slug
  179. entry.headline = headline
  180. entry.body = body
  181. entry.pub_date = pub_date
  182. entry.user = User.objects.get(id=user)
  183. entry.active = active
  184. entry.save()
  185. ################################### Trackback Status ###########################################
  186. class TrackbackStatus(models.Model):
  187. ''' This model holds status for trackbacks from our entries. In other words it
  188. represents outgoing trackbacks.
  189. '''
  190. # Note: If entry is marked edit_inline, deleting a link in an entry will not be
  191. # able to delete the corresponding TrackbackStatus because it will be added back
  192. # in by the admin save handler
  193. #: The entry for which the trackback was posted
  194. entry = models.ForeignKey(Entry, verbose_name='referring entry', related_name='trackbacks')
  195. #: The outside blog entry referenced from our entry
  196. link = models.URLField('entry link', maxlength=300, verify_exists=False, core=True)
  197. #: The link to post the trackback to
  198. trackbackUrl = models.URLField('trackback link', verify_exists=False, blank=True)
  199. #: Datetime of most recent trackback attempt
  200. attempted = models.DateTimeField(null=True, blank=True)
  201. #: Options for status
  202. statusChoices = (
  203. ('NotYet', 'Not attempted'),
  204. ('NoLink', 'No trackback URL found'),
  205. ('Success', 'Success'),
  206. ('Failed', 'Failed'),
  207. ('Dont', 'Do not attempt'),
  208. )
  209. #: Result of most recent trackback attempt
  210. status = models.CharField(maxlength=10, blank=False, choices=statusChoices, default='NotYet')
  211. @property
  212. def eligible(self):
  213. ''' Is this trackback eligible to be attempted? '''
  214. return (self.status=='NotYet'
  215. or (self.status=='Failed' and 'Throttled' in self.message)
  216. or (self.status=='NoLink' and self.trackbackUrl))
  217. #: Additional status info e.g. error message received
  218. message = models.TextField('additional status', blank=True)
  219. class Admin:
  220. list_display = 'link status attempted message'.split()
  221. class Meta:
  222. verbose_name_plural='trackbacks'
  223. ordering = ('link',)
  224. def __str__(self):
  225. return '%s (%s)' % (self.link, self.status)
  226. def appendMessage(self, message):
  227. if self.message:
  228. self.message = self.message + '\n' + message
  229. else:
  230. self.message = message
  231. def attempt(self):
  232. ''' Attempt to post this trackback if it is eligible.
  233. Sets our status code and message.
  234. '''
  235. if not self.eligible:
  236. # Don't retry
  237. return
  238. self.attempted = datetime.datetime.now()
  239. self.message= ''
  240. try:
  241. # Get the trackback URL
  242. if not self.trackbackUrl:
  243. self.trackbackUrl, self.page_data = tc.discover(self.link)
  244. if self.trackbackUrl is None:
  245. # Try appending /trackback/
  246. self.appendMessage('Using /trackback/')
  247. if self.link.endswith('/'):
  248. self.trackbackUrl = self.link + 'trackback/'
  249. else:
  250. self.trackbackUrl = self.link + '/trackback/'
  251. if self.trackbackUrl:
  252. # Do the actual trackback
  253. siteName = Site.objects.get(id=settings.SITE_ID).name
  254. tc.postTrackback(self.trackbackUrl, self.entry.get_absolute_url(), self.entry.headline, self.entry.excerpt, siteName)
  255. self.status = 'Success'
  256. else:
  257. self.status = 'NoLink'
  258. except Exception, e:
  259. self.status = 'Failed'
  260. msg = '%s: %s' % (e.__class__.__name__, e)
  261. # Ensure valid utf-8 with possibly some minor data loss
  262. msg = msg.decode('utf-8', 'replace').encode('utf-8')
  263. self.appendMessage(msg)
  264. self.save()
  265. @staticmethod
  266. def attempt_all_current():
  267. ''' Attempt to trackback all eligible trackback whose Entries are current. '''
  268. now = datetime.datetime.now()
  269. oneWeekAgo = now - datetime.timedelta(days=7)
  270. for tb in TrackbackStatus.objects.exclude(entry__pub_date__gt=now).exclude(entry__pub_date__lt=oneWeekAgo):
  271. tb.attempt()