PageRenderTime 37ms CodeModel.GetById 15ms app.highlight 18ms RepoModel.GetById 1ms app.codeStats 0ms

/blogmaker/blog/models.py

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