models.py 7.19 KB
Newer Older
cynddl's avatar
cynddl committed
1
from django.db import models
cynddl's avatar
cynddl committed
2
from django.utils.translation import ugettext_lazy as _
3
from django.core import files
luxcem's avatar
luxcem committed
4

cynddl's avatar
cynddl committed
5
from taggit.managers import TaggableManager
luxcem's avatar
luxcem committed
6
from newspaper import Article as ArticleParser
luxcem's avatar
luxcem committed
7
from django_und.models import VoteMixin
8
from django_fsm import FSMField, transition, RETURN_VALUE
luxcem's avatar
luxcem committed
9

10
from io import BytesIO
11
from datetime import datetime
12

cynddl's avatar
cynddl committed
13
from rp.utils import cleanup_url
14

cynddl's avatar
cynddl committed
15

16
17
ARTICLE_SCORE_THRESHOLD = 3

luxcem's avatar
luxcem committed
18
STATUS_CHOICES = (
19
20
    ("NEW", _("New")),
    ("DRAFT", _("Draft")),
luxcem's avatar
luxcem committed
21
22
23
24
    ("PUBLISHED", _("Published")),
    ("REJECTED", _("Rejected"))
)

25
26
27
28
29
30
LANG_CHOICES = (
    ("FR", _("French")),
    ("EN", _("English")),
    ("NA", _("Other"))
)

31
32
33
34
35
36
37
38
39
40
URL_HELP_TEXT = """The URL should not contain any marketing tags. We
automatically strip the most known tags."""

TITLE_HELP_TEXT = """Please remove non-necessary parts such as newspapers'
names and leave only the article title."""

EXTRACTS_HELP_TEXT = """Please select short and helpful extracts from the
article content. You should aim at around 500 characters. Use bracket ellipsis
[…] to cut parts not required to understand the context."""

luxcem's avatar
luxcem committed
41

luxcem's avatar
luxcem committed
42
class Article(VoteMixin):
cynddl's avatar
cynddl committed
43
    #: Logical state (eg. article submitted, published, or rejected)
44
45
    status = FSMField(default='NEW', choices=STATUS_CHOICES, protected=True)

cynddl's avatar
cynddl committed
46
    #: Original URL
47
    url = models.URLField("URL", help_text=URL_HELP_TEXT)
cynddl's avatar
cynddl committed
48
49

    #: Language of the webpage
50
51
    lang = models.CharField(
        _("Language"), choices=LANG_CHOICES, default="NA", max_length=50)
cynddl's avatar
cynddl committed
52
53

    #: Plain-text Opengraph metadata
54
55
    metadata = models.TextField(
        _("Opengraph metadata"), blank=True, null=True)
cynddl's avatar
cynddl committed
56
57

    #: Screenshot or banner image for the original webpage
58
59
    screenshot = models.ImageField(
        _("Article screenshot"), blank=True, null=True)
cynddl's avatar
cynddl committed
60
61

    #: Article title
62
63
64
    title = models.CharField(
        _("Article title"), max_length=255, default="",
        help_text=TITLE_HELP_TEXT)
cynddl's avatar
cynddl committed
65
66

    #: Short name for the website (eg. "NY Times")
cynddl's avatar
cynddl committed
67
    website = models.CharField(_("Website"), max_length=255, default="")
cynddl's avatar
cynddl committed
68
69

    #: Short content extracts (eg. two to three paragraphs)
70
71
72
    extracts = models.TextField(
        _("Content extracts"), blank=True, null=True,
        help_text=EXTRACTS_HELP_TEXT)
cynddl's avatar
cynddl committed
73

cynddl's avatar
cynddl committed
74
    #: First submission date
cynddl's avatar
cynddl committed
75
    created_at = models.DateTimeField(_("Creation date"), auto_now_add=True)
cynddl's avatar
cynddl committed
76
77

    #: Name of the user who first submitted the article
78
    created_by = models.CharField(max_length=255, null=True)
cynddl's avatar
cynddl committed
79
80

    #: Last update date
cynddl's avatar
cynddl committed
81
    updated_at = models.DateTimeField(_("Last update"), auto_now=True)
cynddl's avatar
cynddl committed
82
83

    #: Published date
84
85
    published_at = models.DateTimeField(
        _("Publication date"), blank=True, null=True)
cynddl's avatar
cynddl committed
86

luxcem's avatar
luxcem committed
87
88
    #: priority: True if article have priority
    priority = models.BooleanField(default=False)
89

cynddl's avatar
cynddl committed
90
    #: List of short tags to describe the article (eg. "Privacy", "Copyright")
91
    tags = TaggableManager(blank=True)
92

93
94
95
    class Meta:
        verbose_name = _("Article")
        verbose_name_plural = _("Articles")
cynddl's avatar
cynddl committed
96

luxcem's avatar
luxcem committed
97
98
99
100
        permissions = (
            ("can_change_status", "Can change article status"),
            ("can_change_priority", "Can change article priority"),
            ("can_vote", "Can vote articles"),
101
            ("can_edit", "Can edit articles")
luxcem's avatar
luxcem committed
102
        )
luxcem's avatar
luxcem committed
103

cynddl's avatar
cynddl committed
104
        #: By default, sort articles by published, updated, or created date
105
106
        ordering = ["-published_at", "-updated_at", "-created_at"]

luxcem's avatar
luxcem committed
107
    def __str__(self):
cynddl's avatar
cynddl committed
108
        """ Returns article title. """
luxcem's avatar
luxcem committed
109
        return self.title
dave's avatar
dave committed
110

111
    # Finite state logic
112

113
    @transition(field=status, source='DRAFT', target='PUBLISHED',
cynddl's avatar
cynddl committed
114
                permission="rp.can_change_status")
115
    def publish(self):
cynddl's avatar
cynddl committed
116
        """ Publish a complete draft. """
117
118
119
        self.published_at = datetime.now()

    @transition(field=status, source='NEW', target='DRAFT',
cynddl's avatar
cynddl committed
120
                permission="rp.can_change_status")
121
    def recover(self):
cynddl's avatar
cynddl committed
122
        """ Force an article to be considered as a draft. """
123
124
125
        pass

    @transition(field=status, source=['NEW', 'DRAFT'], target='REJECTED',
cynddl's avatar
cynddl committed
126
                permission="rp.can_change_status")
127
    def reject(self):
cynddl's avatar
cynddl committed
128
        """ Manual rejection of the article. """
129
130
131
        pass

    @transition(field=status, source='DRAFT', target='DRAFT',
cynddl's avatar
cynddl committed
132
                permission="rp.can_change_priority")
133
    def set_priority(self):
cynddl's avatar
cynddl committed
134
        """ Set the boolean priority of an article to True. """
135
136
137
        self.priority = True

    @transition(field=status, source='DRAFT', target='DRAFT',
cynddl's avatar
cynddl committed
138
                permission="rp.can_change_priority")
139
    def unset_priority(self):
cynddl's avatar
cynddl committed
140
        """ Set the boolean priority of an article to False. """
141
        self.priority = False
142
143
144

    @transition(field=status, source='DRAFT', target='DRAFT')
    @transition(field=status, source='NEW',
cynddl's avatar
cynddl committed
145
                target=RETURN_VALUE('NEW', 'DRAFT'), permission="rp.can_vote")
146
    def upvote(self, by=None):
cynddl's avatar
cynddl committed
147
148
149
150
151
        """
        Upvote the article score for the given user and remove previous votes.
        If the score crosses the threshold ```ARTICLE_SCORE_THRESHOLD```,
        automatically moves the article from _NEW_ to _DRAFT_.
        """
152
        super(Article, self).upvote(by)
153
154
155
156
157
        if self.und_score >= ARTICLE_SCORE_THRESHOLD:
            return 'DRAFT'
        else:
            return self.status

158
159
    @transition(field=status, source='NEW', target='NEW',
                permission="rp.can_vote")
160
    @transition(field=status, source='DRAFT', target='DRAFT',
cynddl's avatar
cynddl committed
161
                permission="rp.can_vote")
162
    def downvote(self, by=None):
cynddl's avatar
cynddl committed
163
        """
164
165
166
        Downvote the article score for the given user and remove previous
        votes. Draft articles can be downvoted but will not be moved back in
        the _NEW_ queue.
cynddl's avatar
cynddl committed
167
        """
168
        super(Article, self).downvote(by)
169

cynddl's avatar
cynddl committed
170
    def add_new_url(url, by=None):
cynddl's avatar
cynddl committed
171
172
173
174
        """ Manually add a new article from its URL.
        Verify if the article has not been submitted before and automatically
        upvote for the given user if applicable.
        """
cynddl's avatar
cynddl committed
175
176
177
178
        url = cleanup_url(url)
        article, _ = Article.objects.get_or_create(url=url)

        if article.created_by is None:
179
            article.created_by = str(by)
cynddl's avatar
cynddl committed
180
181
182
183
184
185
186

        if by is not None:
            article.upvote(by)

        article.save()
        return article

187
    # Content extraction
188

189
    def fetch_content(self):
cynddl's avatar
cynddl committed
190
        if self.lang != "NA":
191
            article = ArticleParser(url=self.url, language=self.lang.lower())
cynddl's avatar
cynddl committed
192
193
194
        else:
            article = ArticleParser(url=self.url)

dave's avatar
dave committed
195
196
197
198
        article.download()
        article.parse()
        self.title = article.title
        self.extracts = article.text
cynddl's avatar
cynddl committed
199
        self.save()
200

201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
    def fetch_image(self):
        import requests
        import imghdr

        if self.lang != "NA":
            article = ArticleParser(url=self.url, language=self.lang.lower())
        else:
            article = ArticleParser(url=self.url)

        article.download()
        article.parse()

        img_path = article.meta_img
        if img_path:
            resp = requests.get(img_path, stream=True)
            if resp.status_code == requests.codes.ok:
                fp = BytesIO()
                fp.write(resp.content)

                file_name_ext = imghdr.what(None, resp.content)
                self.screenshot.save(
                    "screenshot-{0}.{1}".format(self.id, file_name_ext),
                    files.File(fp), save=True)