Esempio n. 1
0
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)
Esempio n. 2
0
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
Esempio n. 3
0
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)
Esempio n. 4
0
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
Esempio n. 5
0
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()
Esempio n. 6
0
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
Esempio n. 7
0
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 = '&hellip;'
            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', )