class AbstractMessage(Logged, Slugged): newsletter = models.ForeignKey('Newsletter', on_delete=models.CASCADE, related_name='messages', verbose_name=_('Newsletter')) guid = fields.GuidField(max_length=15, editable=False, unique=True, verbose_name=_('Global Unique Identifier')) 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')) is_sent = fields.BooleanField(default=False, editable=False, verbose_name=_('Is Sent?')) class Meta: abstract = True verbose_name = _('Message') verbose_name_plural = _('Messages') def __str__(self): return self.subject @staticmethod def autocomplete_search_fields(): return ('subject__icontains', ) def get_absolute_url(self): kwargs = { #'message_pk': self.pk, #'message_slug': self.slug, 'message_guid': self.guid, } return reverse('message', kwargs=kwargs) def save(self, **kwargs): if not self.text: self.text = extract_text(self.html) super(AbstractMessage, self).save(**kwargs)
class AbstractMessageLink(Logged): guid = fields.GuidField(max_length=15, editable=False, unique=True, verbose_name=_('Global Unique Identifier')) url = models.URLField(editable=True, unique=True, max_length=255, verbose_name=_('URL')) class Meta: abstract = True ordering = ['url'] verbose_name = _('Message Link') verbose_name_plural = _('Message Links') def __str__(self): return self.url
class AbstractMessageImage(Illustrated, Logged): guid = fields.GuidField(max_length=7, editable=False, unique=True, verbose_name=_('Global Unique Identifier')) name = fields.IdentifierField(unique=True, max_length=63, verbose_name=_('Name')) class Meta: abstract = True folder_name = 'newsletters' ordering = ['name'] verbose_name = _('Message Image') verbose_name_plural = _('Message Images') def __str__(self): return self.name def get_upload_path(self, filename): return super(AbstractMessageImage, self).get_upload_path(self.name)
class AbstractAttachment(Logged, Calculated): guid = fields.GuidField(editable=False, verbose_name=_('Global Unique Identifier')) title = fields.CharField(max_length=63, verbose_name=_('Title')) caption = fields.CharField(max_length=255, blank=True, verbose_name=_('Caption')) alt = fields.CharField(max_length=127, blank=True, verbose_name=_('Alternate Text')) description = fields.TextField(blank=True, verbose_name=_('Description')) file = models.FileField(blank=True, max_length=127, upload_to=file_upload_to, verbose_name=_('File')) external_file = models.URLField(blank=True, max_length=127, verbose_name=_('External File')) size = fields.IntegerField(blank=True, calculated=True, min_value=0, null=True, verbose_name=_('Size')) mime_type = fields.CharField(blank=True, calculated=True, max_length=31, null=True, verbose_name=_('MIME Type')) height = fields.IntegerField(blank=True, calculated=True, min_value=0, null=True, verbose_name=_('Height')) width = fields.IntegerField(blank=True, calculated=True, min_value=0, null=True, verbose_name=_('Width')) category = models.ForeignKey('AttachmentCategory', blank=True, null=True, on_delete=models.PROTECT, related_name='attachments', verbose_name=_('Category')) class Meta: abstract = True folder_name = 'attachments' ordering = ['title'] verbose_name = _('Attachment') verbose_name_plural = _('Attachments') def __str__(self): return self.title def clean(self): super(AbstractAttachment, self).clean() if not self.file and not self.external_file: msg = _( 'You must upload a file or set the URL of an external file.') raise ValidationError({'file': msg}) def delete(self, *args, **kwargs): self.file.delete(save=False) return super(AbstractAttachment, self).delete(*args, **kwargs) def get_upload_path(self, filename): if self.title: _, extension = os.path.splitext(filename) return slugify(self.title, ascii=True).replace('-', '_') + extension else: return filename # CUSTOM METHODS def calculate_height(self): if not self.file or not self.is_image: return None else: return self.image.height def calculate_mime_type(self): if self.file and magic is not None: file_type = magic.from_buffer(self.file.read(1024), mime=True) if file_type is not None: return force_text(file_type) file_name = self.get_file_name() file_type, _ = mimetypes.guess_type(file_name) if file_type is not None: return force_text(file_type) if not self.file and magic is not None: file_type = magic.from_buffer(urlopen(file_name).read(1024), mime=True) if file_type is not None: return force_text(file_type) return None def calculate_size(self): if not self.file: return None else: return self.file.size def calculate_width(self): if not self.file or not self.is_image: return None else: return self.image.width def get_audio_tag(self, **attrs): wrap = attrs.pop('wrap', False) attrs['src'] = self.get_file_url() attrs.setdefault('controls', True) attrs.setdefault('preload', 'none') content = self.get_file_link(text=(self.alt or self.title)) tag = make_double_tag('audio', content, attrs) if wrap: tag = make_double_tag('div', tag, {'class': 'audio-wrap'}) return tag def get_display_size(self): if self.size is None: return '' bytes = self.size if bytes < 1024: return '{0} B'.format(number_format(bytes)) kb = (bytes / 1024) if kb < 1024: return '{0} KB'.format(number_format(kb, 1)) mb = (kb / 1024) if mb < 1024: return '{0} MB'.format(number_format(mb, 1)) gb = (mb / 1024) return '{0} GB'.format(number_format(gb, 1)) get_display_size.admin_order_field = 'size' get_display_size.short_description = _('Size') def get_file_link(self, **attrs): attrs['href'] = self.get_file_url() attrs.setdefault('download', True) content = attrs.pop('text', self.title) return make_double_tag('a', content, attrs) def get_file_url(self): if not self.file: return self.external_file else: return self.file.url get_file_url.short_description = _('File') def get_file_name(self): if not self.file: return self.external_file else: return self.file.name get_file_name.short_description = _('File') def get_tag(self, **attrs): if self.is_audio: return self.get_audio_tag(**attrs) if self.is_image: return self.get_image_tag(**attrs) if self.is_video: return self.get_video_tag(**attrs) if self.is_external: url = self.get_file_url() if 'youtube' in url or 'vimeo' in url: return self.get_iframe_tag() return self.get_file_link(**attrs) def get_iframe_tag(self, **attrs): wrap = attrs.pop('wrap', False) attrs['src'] = self.get_file_url() attrs.setdefault('width', self.width or 640) attrs.setdefault('height', self.height or 360) attrs.setdefault('frameborder', 0) attrs.setdefault('webkitallowfullscreen', True) attrs.setdefault('mozallowfullscreen', True) attrs.setdefault('allowfullscreen', True) content = self.get_file_link(text=(self.alt or self.title)) tag = make_double_tag('iframe', content, attrs) if wrap: tag = make_double_tag('div', tag, {'class': 'iframe-wrap'}) return tag def get_image_tag(self, **attrs): wrap = attrs.pop('wrap', False) attrs['src'] = self.get_file_url() if self.width: attrs.setdefault('width', self.width) if self.height: attrs.setdefault('height', self.height) attrs.setdefault('alt', self.alt or self.title) tag = make_single_tag('img', attrs) if wrap: tag = make_double_tag('div', tag, {'class': 'image-wrap'}) return tag def get_video_tag(self, **attrs): wrap = attrs.pop('wrap', False) attrs['src'] = self.get_file_url() attrs.setdefault('width', self.width or 640) attrs.setdefault('height', self.height or 360) attrs.setdefault('controls', True) attrs.setdefault('preload', 'metadata') content = self.get_file_link(text=(self.alt or self.title)) tag = make_double_tag('video', content, attrs) if wrap: tag = make_double_tag('div', tag, {'class': 'video-wrap'}) return tag # PROPERTIES @cached_property def image(self): if not self.file: return None else: return SourceFile(self.file, self.file.name, self.file.storage) @cached_property def is_audio(self): file_url = self.get_file_url() if file_url and file_url.endswith(settings.AUDIO_EXTENSIONS): return True if self.mime_type and self.mime_type.startswith('audio'): return True return False @cached_property def is_external(self): return not self.file @cached_property def is_image(self): file_url = self.get_file_url() if file_url and file_url.endswith(settings.IMAGE_EXTENSIONS): return True if self.mime_type and self.mime_type.startswith('image'): return True return False @cached_property def is_video(self): file_url = self.get_file_url() if file_url and file_url.endswith(settings.VIDEO_EXTENSIONS): return True if self.mime_type and self.mime_type.startswith('video'): return True return False
class AbstractSubscriber(Enableable, Logged): guid = fields.GuidField(max_length=31, editable=False, unique=True, verbose_name=_('Global Unique Identifier')) email_address = fields.EmailField(max_length=127, unique=True, verbose_name=_('E-mail Address')) email_domain = models.ForeignKey('Domain', editable=False, on_delete=models.CASCADE, related_name='subscribers', verbose_name=_('E-mail Domain')) first_name = fields.CharField(blank=True, max_length=63, verbose_name=_('First Name')) last_name = fields.CharField(blank=True, max_length=63, verbose_name=_('Last Name')) newsletters = models.ManyToManyField('Newsletter', through='Subscription', related_name='subscribers', verbose_name=_('Newsletters')) tags = models.ManyToManyField('SubscriberTag', blank=True, related_name='subscribers', verbose_name=_('Tags')) score = fields.FloatField(blank=True, db_index=True, default=2.0, editable=False, verbose_name=_('Score')) class Meta: abstract = True ordering = ['email_address'] verbose_name = _('Subscriber') verbose_name_plural = _('Subscribers') def __str__(self): return self.email_address @staticmethod def autocomplete_search_fields(): return ('email_address__icontains', 'first_name__icontains', 'last_name__icontains') # CUSTOM METHODS def is_subscribed_to(self, newsletter): if not self._get_pk_val(): return False else: return self.subscriptions.filter(newsletter=newsletter).exists() def set_email(self, address): address = normalize_email(address) if not validate_email(address): msg = "'{0}' is not a valid email address." raise ValueError(msg.format(address)) _, domain_name = address.rsplit('@', 1) domain, _ = Domain.objects.get_or_create(name=domain_name) self.email_address = address self.email_domain = domain def resubscribe_to(self, newsletter): if not self.is_subscribed_to(newsletter): qs = self.unsubscriptions.filter(newsletter=newsletter) unsubscription = qs.order_by('date').last() if unsubscription is not None: unsubscription.delete() return self.subscriptions.create(newsletter=newsletter) def subscribe_to(self, newsletter): if self.is_subscribed_to(newsletter): return None else: return self.subscriptions.create(newsletter=newsletter) def unsubscribe_from(self, newsletter, reason=None, last_message=None): if self.is_subscribed_to(newsletter): self.subscriptions.filter(newsletter=newsletter).delete() kwargs = { 'newsletter': newsletter, 'reason': reason, 'last_message': last_message, } return self.unsubscriptions.create(**kwargs) # PROPERTIES @described_property(_('Name')) def full_name(self): return ' '.join(( self.first_name, self.last_name, )).strip()
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', )