Projet

Général

Profil

Télécharger (7,8 ko) Statistiques
| Branche: | Tag: | Révision:

root / corbo / models.py @ e147eff6

1
import os
2
import io
3
import hashlib
4
from time import mktime
5
from datetime import datetime
6
import logging
7
import urlparse
8
from html2text import HTML2Text
9
from emails.django import Message
10
from lxml import etree
11
import requests
12
import feedparser
13

    
14
from django.utils import timezone
15
from django.conf import settings
16
from django.db import models
17
from django.core.files.storage import DefaultStorage
18
from django.utils.translation import ugettext_lazy as _
19
from django.core import signing
20
from django.template import loader, Context
21
from django.core.urlresolvers import reverse
22

    
23
from ckeditor.fields import RichTextField
24

    
25
channel_choices = (
26
    ('mailto', _('Email')),
27
)
28

    
29
UNSUBSCRIBE_LINK_PLACEHOLDER = '##UNSUBSCRIBE_LINK_PLACEHOLDER##'
30

    
31
logger = logging.getLogger(__name__)
32

    
33

    
34
def transform_image_src(src, **kwargs):
35
    basename = os.path.basename(src)
36
    if basename == src:
37
        return src
38
    name, ext = os.path.splitext(src)
39
    hash = hashlib.sha256(name)
40
    return '%s_%s%s' % (os.path.basename(name), hash.hexdigest()[:8], ext)
41

    
42

    
43
class Category(models.Model):
44
    name = models.CharField(_('Name'), max_length=64, blank=False, null=False)
45
    rss_feed_url = models.URLField(_('Feed URL'), blank=True, null=True,
46
                help_text=_('if defined, announces will be automatically created from rss items'))
47
    ctime = models.DateTimeField(auto_now_add=True)
48

    
49
    def __unicode__(self):
50
        return self.name
51

    
52
    def get_announces_count(self):
53
        return self.announce_set.all().count()
54

    
55
    def get_subscriptions_count(self):
56
        return self.subscription_set.all().count()
57

    
58
    def save(self, *args, **kwargs):
59
        super(Category, self).save(*args, **kwargs)
60
        if not self.rss_feed_url:
61
            return
62
        feed_response = requests.get(self.rss_feed_url)
63
        if feed_response.ok:
64
            content = feedparser.parse(feed_response.content)
65
            for entry in content.get('entries', []):
66
                substitutions = []
67
                published = datetime.fromtimestamp(mktime(entry.published_parsed))
68
                html_tree = etree.HTML(entry['summary'])
69
                storage = DefaultStorage()
70
                for img in html_tree.xpath('//img'):
71
                    image_name = os.path.basename(img.attrib['src'])
72
                    r = requests.get(img.attrib['src'])
73
                    new_content = r.content
74
                    if storage.exists(image_name):
75
                        old_content = storage.open(image_name).read()
76
                        old_hash = hashlib.md5(old_content).hexdigest()
77
                        new_hash = hashlib.md5(new_content).hexdigest()
78
                        img.attrib['src'] = storage.url(image_name)
79
                        if new_hash == old_hash:
80
                            continue
81
                    new_image_name = storage.save(image_name, io.BytesIO(new_content))
82
                    img.attrib['src'] = storage.url(new_image_name)
83

    
84
                announce, created = Announce.objects.get_or_create(identifier=entry['id'],
85
                                    category=self)
86
                announce.title = entry['title']
87
                announce.text = etree.tostring(html_tree)
88
                announce.publication_time = published
89
                announce.save()
90

    
91
                if created:
92
                    Broadcast.objects.get_or_create(announce=announce)
93

    
94

    
95
class Announce(models.Model):
96
    category  = models.ForeignKey('Category', verbose_name=_('category'))
97
    title = models.CharField(_('title'), max_length=256,
98
                             help_text=_('maximum 256 characters'))
99
    identifier = models.CharField(max_length=256, null=True, blank=True)
100
    text = RichTextField(_('Content'))
101
    publication_time = models.DateTimeField(_('publication time'), blank=True,
102
                                            null=True)
103
    expiration_time = models.DateTimeField(_('Expires on'), blank=True,
104
                                           null=True)
105
    ctime = models.DateTimeField(_('creation time'), auto_now_add=True)
106
    mtime = models.DateTimeField(_('modification time'), auto_now=True)
107

    
108
    def __unicode__(self):
109
        return u'{title} ({id}) at {mtime}'.format(title=self.title,
110
                                    id=self.id, mtime=self.mtime)
111

    
112
    def is_expired(self):
113
        if self.expiration_time:
114
            return self.expiration_time < timezone.now()
115
        return False
116

    
117
    def is_published(self):
118
        if self.publication_time:
119
            return self.publication_time <= timezone.now()
120
        return False
121

    
122
    class Meta:
123
        verbose_name = _('announce')
124
        ordering = ('-mtime',)
125

    
126

    
127
class Broadcast(models.Model):
128
    announce = models.ForeignKey(Announce, verbose_name=_('announce'))
129
    deliver_time = models.DateTimeField(_('Deliver time'), null=True)
130
    result = models.TextField(_('result'), blank=True)
131

    
132
    def __unicode__(self):
133
        if self.deliver_time:
134
            return u'announce {id} delivered via at {time}'.format(
135
                id=self.announce.id, time=self.deliver_time)
136
        return u'announce {id} to deliver'.format(id=self.announce.id)
137

    
138
    def send(self):
139
        subscriptions = self.announce.category.subscription_set.all()
140
        total_sent = 0
141
        handler = HTML2Text()
142
        template = loader.get_template('corbo/announce.html')
143
        message = Message(subject=self.announce.title, mail_from=settings.CORBO_DEFAULT_FROM_EMAIL,
144
                    html=template.render(Context({'content': self.announce.text,
145
                    'unsubscribe_link_placeholder': UNSUBSCRIBE_LINK_PLACEHOLDER})))
146
        html_tree = etree.HTML(self.announce.text)
147
        storage = DefaultStorage()
148
        for img in html_tree.xpath('//img/@src'):
149
            img_path = img.lstrip(storage.base_url)
150
            message.attach(filename=transform_image_src(img), data=storage.open(img_path))
151

    
152
        message.transformer.apply_to_images(func=transform_image_src)
153
        # perform transformations in message html, like inline css parsing
154
        message.transformer.load_and_transform()
155
        # mark all attached images as inline
156
        message.transformer.make_all_images_inline()
157
        message.transformer.save()
158
        for s in subscriptions:
159
            if not s.identifier:
160
                continue
161
            unsubscribe_token = signing.dumps({'category': self.announce.category.pk,
162
                                               'identifier': s.identifier})
163
            unsubscribe_link = urlparse.urljoin(settings.SITE_BASE_URL, reverse('unsubscribe',
164
                                    kwargs={'unsubscription_token': unsubscribe_token}))
165
            message.html = message.html.replace(UNSUBSCRIBE_LINK_PLACEHOLDER, unsubscribe_link)
166
            handler.body_width = 0
167
            message.text = handler.handle(message.html)
168
            sent = message.send(to=s.identifier)
169
            if sent:
170
                total_sent += 1
171
                logger.info('Announce "%s" sent to %s', self.announce.title, s.identifier)
172
            else:
173
                logger.warning('Error occured while sending announce "%s" to %s.',
174
                               self.announce.title, s.identifier)
175
        self.result = total_sent
176
        self.deliver_time = timezone.now()
177
        self.save()
178

    
179
    class Meta:
180
        verbose_name = _('sent')
181
        ordering = ('-deliver_time',)
182

    
183

    
184
class Subscription(models.Model):
185
    category = models.ForeignKey('Category', verbose_name=_('Category'))
186
    uuid = models.CharField(_('User identifier'), max_length=128, blank=True)
187
    identifier = models.CharField(_('identifier'), max_length=128, blank=True,
188
            help_text=_('ex.: mailto, ...'))
189

    
190
    def get_identifier_display(self):
191
        try:
192
            scheme, identifier = self.identifier.split(':')
193
            return identifier
194
        except ValueError:
195
            return self.identifier
196

    
197
    class Meta:
198
        unique_together = ('category', 'identifier', 'uuid')
(6-6/11)