class AbstractLink(Illustrated, Logged): blog = fields.CachedForeignKey('blogs.Blog', related_name='links', verbose_name=_('Blog')) name = fields.CharField( max_length=63, verbose_name=_('Name'), help_text=_('Example: A framework for perfectionists')) url = models.URLField( max_length=127, verbose_name=_('Web Address'), help_text= _("Example: <code>http://www.djangoproject.com/</code> — don't forget the <code>http://</code>" )) rss = models.URLField( blank=True, max_length=127, verbose_name=_('RSS Address'), help_text= _("Example: <code>http://www.djangoproject.com/rss.xml</code> — don't forget the <code>http://</code>" )) description = fields.CharField( max_length=255, blank=True, verbose_name=_('Description'), help_text= _('This will be shown when someone hovers the link in the blogroll, or optionally below the link.' )) category = fields.CachedForeignKey('LinkCategory', blank=True, null=True, on_delete=models.PROTECT, related_name='links', verbose_name=_('Category')) class Meta: abstract = True folder_name = 'blog_links' ordering = ['name'] verbose_name = _('Link') verbose_name_plural = _('Links') def __str__(self): return self.name def get_upload_path(self, filename): filename = slugify(self.name, ascii=True).replace('-', '_') return super(AbstractLink, self).get_upload_path(filename)
class AbstractCategory(Illustrated, Slugged, MetaData): blog = fields.CachedForeignKey('blogs.Blog', related_name='categories', verbose_name=_('Blog')) name = fields.CharField(unique=True, max_length=63, verbose_name=_('Name')) description = fields.TextField( blank=True, verbose_name=_('Description'), help_text=_('The description is usually not prominent.')) class Meta: abstract = True folder_name = 'blog_categories' ordering = ['name'] verbose_name = _('Category') verbose_name_plural = _('Categories') def __str__(self): return self.name def get_absolute_url(self): kwargs = {'category_slug': self.slug} if settings.BLOG_MULTIPLE: kwargs['blog_slug'] = self.blog.slug return reverse('post_list', kwargs=kwargs) def get_feed_url(self): kwargs = {'category_slug': self.slug} if settings.BLOG_MULTIPLE: kwargs['blog_slug'] = self.blog.slug return full_reverse('post_feed', kwargs=kwargs) def get_upload_path(self, filename): filename = self.slug.replace('-', '_') return super(AbstractCategory, self).get_upload_path(filename) # GRAPPELLI SETTINGS @staticmethod def autocomplete_search_fields(): return ('name__icontains', )
class AbstractLinkCategory(Slugged): blog = fields.CachedForeignKey('blogs.Blog', related_name='link_categories', verbose_name=_('Blog')) name = fields.CharField(unique=True, max_length=63, verbose_name=_('Name')) description = fields.TextField( blank=True, verbose_name=_('Description'), help_text=_('The description is usually not prominent.')) objects = models.Manager() cache = LookupTable(indexed_fields=['slug'], default_registry_key='links:DEFAULT_CATEGORY') class Meta: abstract = True ordering = ['name'] verbose_name = _('Link Category') verbose_name_plural = _('Link Categories') def __str__(self): return self.name def get_absolute_url(self): kwargs = {'link_category_slug': self.slug} if settings.BLOG_MULTIPLE: kwargs['blog_slug'] = self.blog.slug return reverse('link_list', kwargs=kwargs) # GRAPPELLI SETTINGS @staticmethod def autocomplete_search_fields(): return ('name__icontains', )
class AbstractMessage(Logged): connection = fields.CachedForeignKey('Connection', on_delete=models.CASCADE, related_name='messages', verbose_name=_('Connection')) name = fields.CharField(unique=True, max_length=63, verbose_name=_('Name')) sender_name = fields.CharField(max_length=127, verbose_name=_("Sender's Name")) sender_address = fields.CharField(max_length=127, verbose_name=_("Sender's Address")) recipient_name = fields.CharField(blank=True, max_length=255, verbose_name=_("Recipient's Name")) recipient_address = fields.CharField( blank=True, max_length=255, verbose_name=_("Recipient's Address"), help_text=_( 'If field is blank, it will be populated when the message is sent.' )) reply_to_name = fields.CharField(blank=True, max_length=127, verbose_name=_("Reply To Name")) reply_to_address = fields.CharField(blank=True, max_length=127, verbose_name=_("Reply To Address")) subject = fields.CharField(max_length=255, verbose_name=_('Subject')) html = fields.TextField(verbose_name=_('HTML Version')) text = fields.TextField(blank=True, verbose_name=_('Plain Text Version')) class Meta: abstract = True ordering = ['name'] verbose_name = _('Message') verbose_name_plural = _('Messages') def __str__(self): return self.name def save(self, **kwargs): if not self.text: self.text = extract_text(self.html) super(AbstractMessage, self).save(**kwargs) # CUSTOM METHODS def render(self, context=None): if not isinstance(context, Context): context = Context(context) email = EmailMultiAlternatives( Template(self.subject).render(context), Template(self.text).render(context), self.sender, self.recipient, connection=self.connection, ) email.attach_alternative( Template(self.html).render(context), 'text/html', ) if self.reply_to_address: email.extra_headers['Reply-To'] = self.reply_to return email def render_and_send(self, recipient_list, reply_to=None, context=None, connection=None): to = [] for recipient in recipient_list: if isinstance(recipient, (tuple, list)): name = recipient[0].strip() address = recipient[1].strip() if name: to.append('"{0}" <{1}>'.format(name, address)) else: to.append(address) else: to.append(recipient) if isinstance(reply_to, (tuple, list)): name = reply_to[0].strip() address = reply_to[1].strip() if name: reply_to = '"{0}" <{1}>'.format(name, address) else: reply_to = address email = self.render(context) if not email.to: email.to = to if 'Reply-To' not in email.extra_headers and reply_to: email.extra_headers['Reply-To'] = reply_to if connection is not None: email.connection = connection email.send() # PROPERTIES @described_property(_('Recipient'), cached=True) def recipient(self): recipient_list = [] if self.recipient_address.strip(): name_list = self.recipient_name.split(',') address_list = self.recipient_address.split(',') for i, address in enumerate(address_list): address = address.strip() if len(name_list) > i: name = name_list[i].strip() recipient_list.append('"{0}" <{1}>'.format(name, address)) else: recipient_list.append(address) return recipient_list @described_property(_('Reply To'), cached=True) def reply_to(self): if self.reply_to_name: return '"{0}" <{1}>'.format(self.reply_to_name, self.reply_to_address) elif self.reply_to_address: return self.reply_to_address else: return None @described_property(_('Sender'), cached=True) def sender(self): if self.sender_name: return '"{0}" <{1}>'.format(self.sender_name, self.sender_address) elif self.sender_address: return self.sender_address else: return None # GRAPPELLI SETTINGS @staticmethod def autocomplete_search_fields(): return ('name__icontains', 'subject__icontains')
class AbstractNewsletter(Orderable, Logged, Slugged, MetaData): """ A regularly distributed publication to which subscribers can subscribe. """ connection = fields.CachedForeignKey('emails.Connection', on_delete=models.CASCADE, related_name='newsletters', verbose_name=_('E-mail Connection')) guid = fields.GuidField(max_length=7, editable=False, unique=True, verbose_name=_('Global Unique Identifier')) name = fields.CharField(unique=True, max_length=63, verbose_name=_('Name')) description = fields.RichTextField(blank=True, verbose_name=_('Description')) is_published = fields.BooleanField(default=True, verbose_name=_('Is Published?')) sender_name = fields.CharField(max_length=127, verbose_name=_("Sender's Name")) sender_address = fields.CharField(max_length=127, verbose_name=_("Sender's Address")) reply_to_name = fields.CharField(blank=True, max_length=127, verbose_name=_("Reply To Name")) reply_to_address = fields.CharField(blank=True, max_length=127, verbose_name=_("Reply To Address")) return_path_name = fields.CharField(blank=True, max_length=127, verbose_name=_("Return To Name")) return_path_address = fields.CharField(blank=True, max_length=127, verbose_name=_("Return To Address")) objects = NewsletterManager() cache = LookupTable(['guid', 'name']) class Meta: abstract = True verbose_name = _('Newsletter') verbose_name_plural = _('Newsletters') def __str__(self): return self.name @staticmethod def autocomplete_search_fields(): return ('name__icontains', ) # CUSTOM METHODS def get_default_meta_index(self): if self.is_published: return super(AbstractNewsletter, self).get_default_meta_index() else: return False # PROPERTIES @described_property(_('Reply To')) def reply_to(self): if self.reply_to_name: return '"{0}" <{1}>'.format(self.reply_to_name, self.reply_to_address) elif self.reply_to_address: return self.reply_to_address else: return None @described_property(_('Return Path')) def return_path(self): if self.return_path_name: return '"{0}" <{1}>'.format(self.return_path_name, self.return_path_address) elif self.return_path_address: return self.return_path_address else: return None @described_property(_('Sender')) def sender(self): if self.sender_name: return '"{0}" <{1}>'.format(self.sender_name, self.sender_address) elif self.sender_address: return self.sender_address else: return None
class AbstractPost(Illustrated, Displayable, Logged): blog = fields.CachedForeignKey('blogs.Blog', related_name='posts', verbose_name=_('Blog')) guid = fields.GuidField(editable=False, verbose_name=_('Global Unique Identifier')) title = fields.CharField(max_length=255, verbose_name=_('Title')) subtitle = fields.CharField( blank=True, max_length=255, verbose_name=_('Subtitle'), help_text=_('Subtitles are optional complements of your title.')) excerpt = fields.RichTextField( blank=True, verbose_name=_('Excerpt'), help_text=_( 'Excerpts are optional hand-crafted summaries of your content.')) content = fields.RichTextField(blank=True, processors=[attachment_tags], verbose_name=_('Content')) authors = models.ManyToManyField('authors.Author', limit_choices_to={'is_staff': True}, related_name='posts', verbose_name=_('Authors')) categories = models.ManyToManyField('Category', blank=True, related_name='posts', verbose_name=_('Categories')) tags = models.ManyToManyField('Tag', blank=True, related_name='posts', verbose_name=_('Tags')) comment_status = fields.BooleanField( default=lambda: registry['posts:ALLOW_COMMENTS'], verbose_name=_('Allow comments on this post')) ping_status = fields.BooleanField( default=True, verbose_name=_('Allow trackbacks and pingbacks to this post')) views_count = models.PositiveIntegerField(default=0, blank=True, editable=False, db_index=True, verbose_name=_('Views')) comment_count = models.PositiveIntegerField(default=0, blank=True, editable=False, db_index=True, verbose_name=_('Comments')) ping_count = models.PositiveIntegerField(default=0, blank=True, editable=False, verbose_name=_('Pings')) score = models.PositiveIntegerField(default=0, blank=True, editable=False, db_index=True, verbose_name=_('Score')) objects = PostManager() search_fields = {'title': 5, 'subtitle': 3, 'content': 1} class Meta: abstract = True folder_name = 'blog_posts' index_together = [ ('publish_status', 'creation_date'), ] ordering = ['-publish_from'] verbose_name = _('Post') verbose_name_plural = _('Posts') def __str__(self): return self.title def get_absolute_url(self): kwargs = { 'post_slug': self.slug, 'post_guid': self.guid, } if settings.BLOG_MULTIPLE: kwargs['blog_slug'] = self.blog.slug return reverse('post_detail', kwargs=kwargs) def get_feed_url(self): kwargs = { 'post_slug': self.slug, 'post_guid': self.guid, } if settings.BLOG_MULTIPLE: kwargs['blog_slug'] = self.blog.slug return full_reverse('comment_feed', kwargs=kwargs) # CUSTOM METHODS def allow_comments(self): if not self.comment_status or not self.is_published(): return False limit = registry['comments:MAX_DAYS'] if not limit: return True limit = timezone.now() - timedelta(limit) if self.publish_from: return (self.publish_from >= limit) elif self.creation_date: return (self.creation_date >= limit) else: return False allow_comments.boolean = True allow_comments.short_description = _('Allow comments?') def allow_pings(self): return (self.ping_status == True and self.is_published()) allow_pings.boolean = True allow_pings.short_description = _('Allow pings?') def get_comments(self, limit=None, status=None, order_by=None): qs = self.comments.all() qs = qs.prefetch_related('author') if status is None: qs = qs.published() elif isinstance(status, six.string_types): qs = qs.filter(status__api_id=status) else: qs = qs.filter(status=status) if order_by is None: qs = qs.order_by('-creation_date') else: qs = qs.order_by(order_by) if limit: qs = qs[:limit] return qs def get_content(self): return mark_safe(self.content_html) get_content.short_description = _('Content') def get_excerpt(self, max_words=55, end_text='...'): if not self.excerpt_html: truncator = Truncator(self.content_html) if end_text == '...': end_text = '…' return mark_safe(truncator.words(max_words, end_text, html=True)) else: return mark_safe(self.excerpt_html) get_excerpt.short_description = _('Excerpt') def get_next_in_order(self, user=None, same_blog=True): qs = self.__class__._default_manager.published(user) qs = qs.filter(publish_from__gt=self.publish_from) if same_blog: qs = qs.filter(blog_id=self.blog_id) qs = qs.order_by('publish_from') return qs.first() def get_previous_in_order(self, user=None, same_blog=True): qs = self.__class__._default_manager.published(user) qs = qs.filter(publish_from__lt=self.publish_from) if same_blog: qs = qs.filter(blog_id=self.blog_id) qs = qs.order_by('-publish_from') return qs.first() def get_publish_date(self): return self.publish_from if self.publish_from else FakeDate(0, 0, 0) get_publish_date.short_description = _('Publish Date') def get_related_posts(self, limit=5, same_blog=True): manager = self.__class__._default_manager qs = manager.published().exclude(pk__exact=self.pk) if same_blog: qs = qs.filter(blog_id=self.blog_id) def get_primary_keys(posts): if isinstance(posts, QuerySet): return list(posts.values_list('pk', flat=True)) else: return [post.pk for post in posts] # Search similar posts using the post's title. query = ' '.join([ term for term in [ term.strip(punctuation) # This transforms for term # "Super toy: pack 10 u." in self.title.split() # into if not term.endswith('.') # "Super toy pack" ] if not term.isdigit() ]) related_post_ids = get_primary_keys(list(qs.search(query)[:limit])) remaining = limit - len(related_post_ids) if remaining > 0: # Fetch post from post's categories. related_post_ids += get_primary_keys( qs.filter(categories__in=self.categories.all()).exclude( pk__in=related_post_ids). distinct( # Call to `distinct()` is required ).order_by( # because `categories__in` filter '-score' # may result in duplicates. )[:remaining]) remaining = limit - len(related_post_ids) if remaining > 0: # Fetch post from the rest of the blog. related_post_ids += get_primary_keys( qs.exclude(pk__in=related_post_ids).order_by('-score') [:remaining]) remaining = limit - len(related_post_ids) related_posts = manager.filter( pk__in=related_post_ids).order_by('-score') return related_posts def increase_comment_count(self): record, _ = PostRecord.objects.get_or_create(post=self) record.comment_count = F('comment_count') + 1 record.save(update_fields=['comment_count']) def increase_ping_count(self): record, _ = PostRecord.objects.get_or_create(post=self) record.ping_count = F('ping_count') + 1 record.save(update_fields=['ping_count']) def increase_views_count(self): record, _ = PostRecord.objects.get_or_create(post=self) record.views_count = F('views_count') + 1 record.save(update_fields=['views_count']) # GRAPPELLI SETTINGS @staticmethod def autocomplete_search_fields(): return ('title__icontains', )
class BaseComment(Nestable, Logged, Linked): parent = ParentForeignKey('self', blank=True, null=True, related_name='children', verbose_name=_('Parent')) author_name = fields.CharField(blank=False, max_length=63, verbose_name=_('Name')) author_email = fields.EmailField(blank=False, max_length=63, verbose_name=_('Email Address')) author_url = models.URLField(blank=True, max_length=127, verbose_name=_('URL')) ip_address = models.GenericIPAddressField(blank=True, null=True, protocol='both', unpack_ipv4=True, verbose_name=_('IP Address')) user_agent = fields.TextField(blank=True, verbose_name=_('User Agent')) karma = fields.IntegerField(default=0, blank=True, verbose_name=_('Karma')) status = fields.CachedForeignKey('CommentStatus', on_delete=models.PROTECT, related_name='comments', verbose_name=_('Status')) is_published = fields.BooleanField(editable=False, default=True, verbose_name=_('Is Published?')) objects = CommentManager() class Meta: abstract = True index_together = [('status', 'creation_date')] ordering = ['-creation_date'] permissions = [('can_moderate', _('Can moderate comments'))] verbose_name = _('Comment') verbose_name_plural = _('Comments') _author_email = Undefined _author_name = Undefined def __init__(self, *args, **kwargs): super(BaseComment, self).__init__(*args, **kwargs) self.old_status_id = self.status_id def __str__(self): args = ( self.pk, self.get_author_name(), ) return '#{0} {1}'.format(*args) def save(self, **kwargs): updated_fields = kwargs.get('update_fields', ()) new_record = (kwargs.get('force_insert', False) or not (self.pk or updated_fields)) if (not updated_fields and (new_record or self.status_id != self.old_status_id)): status = self.get_status() self.is_published = status.publish_comment self.old_status_id = status.id self._author_email = Undefined self._author_name = Undefined super(BaseComment, self).save(**kwargs) def get_absolute_url(self): if not hasattr(self, 'post'): msg = ('{cls} is missing a post. Define {cls}.post, or override ' '{cls}.get_absolute_url().') raise ImproperlyConfigured(msg.format(cls=self.__class__.__name__)) else: return '#'.join((self.post.get_absolute_url(), self.get_anchor())) # CUSTOM METHODS def get_anchor(self): return settings.COMMENT_ANCHOR_PATTERN.format(**self.get_anchor_data()) def get_anchor_data(self): data = { 'id': self.id, 'parent_id': self.parent_id, } if hasattr(self, 'post_id'): data['post_id'] = self.post_id elif hasattr(self, 'post'): data['post_id'] = self.post.pk return data def get_author_email(self): if self._author_email is Undefined: if not hasattr(self, 'author') or self.author is None: self._author_email = self.author_email else: self._author_email = self.author.email return self._author_email get_author_email.short_description = _('Email Address') def get_author_link(self): url = self.get_author_url() name = conditional_escape(self.get_author_name()) if url: return mark_safe('<a href="{0}" rel="nofollow">{1}</a>'.format( url, name)) else: return mark_safe(name) get_author_link.short_description = _('Author') def get_author_name(self): if self._author_name is Undefined: if not hasattr(self, 'author') or self.author is None: self._author_name = self.author_name else: self._author_name = (self.author.get_full_name() or self.author.get_username()) return self._author_name get_author_name.short_description = _('Name') def get_author_url(self): return self.author_url get_author_url.short_description = _('URL') def get_children(self, limit=None, status=None, order_by=None): qs = self.children.all() qs = qs.prefetch_related('author') if status is None: qs = qs.published() elif isinstance(status, six.string_types): qs = qs.filter(status__api_id=status) else: qs = qs.filter(status=status) if order_by is None: qs = qs.order_by('-creation_date') else: qs = qs.order_by(order_by) if limit: qs = qs[:limit] return qs def get_content(self): status = self.get_status() if status.comment_replacement: return mark_safe(status.comment_replacement) elif hasattr(self, 'content_html'): return mark_safe(self.content_html) elif hasattr(self, 'content'): return mark_safe(linebreaks(self.content, autoescape=True)) else: msg = ('{cls} is missing a content. Define {cls}.content_html, ' '{cls}.content, or override {cls}.get_content().') raise ImproperlyConfigured(msg.format(cls=self.__class__.__name__)) get_content.short_description = _('Content') def get_date(self): return self.creation_date get_date.short_description = _('Date') def get_excerpt(self, max_words=20, end_text='...'): content = self.get_content() if hasattr(content, '__html__'): # The __html__ attribute means the content was previously # marked as safe, so can include HTML tags. truncator = Truncator(content.__html__()) if end_text == '...': end_text = '…' return mark_safe(truncator.words(max_words, end_text, html=True)) else: return Truncator(content).words(max_words, end_text, html=False) get_excerpt.short_description = _('Excerpt') def get_link(self): url = self.get_absolute_url() text = ugettext('{author} on {post}').format( **{ 'author': self.get_author_name(), 'post': self.get_post(), }) return mark_safe('<a href="{0}">{1}</a>'.format(url, escape(text))) def get_status(self): return self.status get_excerpt.short_description = _('Status')