blog

models.py

1
from django.contrib.auth.models import User  # For logging purposes
2
from django.utils import translation
3
from django.template.defaultfilters import slugify
4
from django.db import models
5
from django.conf import settings  # Necessary to get the link to the media root folder
6
from django.utils.translation import gettext as _
7
import pytz
8
import datetime
9
import os
10
import subprocess
11
12
from django.shortcuts import render as render_shortcut
13
14
""" New version:
15
- For each post, there's no longer a mandatory Dutch and English
16
  version. Instead, only the title needs to be in multiple languages.
17
- There's a new table for the links to the articles themselves. These include a
18
  language code and a foreign key to the post they belong to.
19
- If an article is available in the active language, but not tagged for the same
20
  dialect, then it should just show up without any warnings.
21
- If an article is not available in the active language, only the title should
22
  show up, but where the short intro text would normally be, there should be an
23
  explanation that it's only available in other languages, and provide links to
24
  those versions.
25
"""
26
27
# Look, you think this function is worthless, it's not. It's required to make
28
# migrations with manage.py, so here it stays, being empty and hollow like the
29
# piece of shit it is.
30
def post_title_directory():
31
    pass
32
        
33
34
class Post(models.Model):
35
    """ Represents a blog post."""
36
    published = models.DateTimeField(auto_now_add=True)
37
    visible = models.BooleanField(default=True, 
38
            help_text="Whether this post is shown in the index. If False, it's \
39
            only accessible by direct link.")
40
    # TODO: The titles should all be changed to 'unique=True' to avoid slug
41
    # collisions. But at this moment, there are still some posts that don't have
42
    # a title in all these languages (and "" collides with ""), so until that's
43
    # fixed, they're set to False.
44
    title_en = models.CharField(max_length=64, unique=False, blank=False)
45
    title_nl = models.CharField(max_length=64, unique=False, blank=False)
46
    title_fr = models.CharField(max_length=64, unique=False, blank=True)
47
    title_de = models.CharField(max_length=64, unique=False, blank=True)
48
    title_es = models.CharField(max_length=64, unique=False, blank=True)
49
    title_eo = models.CharField(max_length=64, unique=False, blank=True)
50
    title_af = models.CharField(max_length=64, unique=False, blank=True)
51
    title_nl_be=models.CharField(max_length=64, unique=False, blank=True)
52
    title_fr_be=models.CharField(max_length=64, unique=False, blank=True)
53
54
55
    def __str__(self):
56
        return self.title()
57
58
59
    def articles(self):
60
        #print(len(Article.objects.filter(post=self)))
61
        return Article.objects.filter(post=self)
62
        
63
    def article(self):
64
        language_code = translation.get_language()
65
        #print(language_code)
66
        # Retrieves all articles that have this post as their foreign key
67
        articles = Article.objects.filter(post=self)
68
        for a in articles:
69
            if a.language_code == language_code:
70
                return a
71
        # If no exact match was found, try again, but now accept other dialects
72
        # as well:
73
        for a in articles:
74
            if a.language_code.startswith(language_code):
75
                return a
76
        
77
        # If still no article was found, return None
78
        return None
79
80
    def title(self):
81
        language_code = translation.get_language()
82
        options = {'af': self.title_af,
83
                   'de': self.title_de,
84
                   'es': self.title_es,
85
                   'en': self.title_en,
86
                   'eo': self.title_eo,
87
                   'fr': self.title_fr,
88
                   'nl-be': self.title_nl_be,
89
                   'fr-be': self.title_fr_be,
90
                   'nl': self.title_nl}
91
        for code, translated_title in options.items():
92
            if language_code.startswith(code):
93
                return translated_title
94
        # If no return has happened, default to English
95
        return self.title_en
96
97
def org_to_html(file_path, slug, return_djhtml_path=False):
98
    """ Converts the given org formatted file to HTML.
99
    This function directly returns the resulting HTML code. This function uses
100
    the amazing Haskell library Pandoc to convert the file (and takes care
101
    of header id's and all that stuff).
102
    """
103
    # FIXME: Remove hardcoded link to media. Replace with media tag!
104
    # XXX: The reason I'm first converting all occurences of .jpg][ and .png][
105
    # to .jpgPANDOCBUG][ and .pngPANDOCBUG][, is because of a Pandoc bug that
106
    # removes the text links for images. It is afterwards converted back, no
107
    # worries.
108
    file = open(file_path, "r", encoding="utf-8")
109
    text = file.read()
110
    file.close()
111
    text = text.replace(".jpg][", ".jpgPANDOCBUG][")
112
    text = text.replace(".png][", ".pngPANDOCBUG][")
113
    file = open("/tmp/blog-file-"+slug+".org", "w", encoding="utf-8")
114
    file.write(text)
115
    file.close()
116
    # --wrap=none is necessary for the Article.headings method to function
117
    # properly, otherwise the heuristic fails on a regular basis
118
    html_text = subprocess.check_output(["pandoc", "--from=org", "--to=html", "--wrap=none", "/tmp/blog-file-"+slug+".org"])
119
    html_text = html_text.decode("utf-8").replace(".jpgPANDOCBUG", ".jpg")
120
    html_text = html_text.replace(".pngPANDOCBUG", ".png")
121
    # Detecting where the footnote section starts, and removing that tag
122
    html_text = html_text.replace('<aside id="footnotes" class="footnotes footnotes-end-of-document" role="doc-endnotes">\n<hr />', _("</section><section><h2>Footnotes</h2>"))
123
    html_text = html_text.replace('</aside>',"")
124
    #rendered_file_path = "file_path.rpartition('.')[0] + ".djhtml"
125
    rendered_file_path = "/tmp/blog-file-"+slug+".djhtml"
126
    rendered_file = open(rendered_file_path, "w", encoding="utf-8")
127
    rendered_file.write(html_text)
128
    rendered_file.close()
129
    if return_djhtml_path:
130
        return rendered_file_path
131
    else:
132
        return html_text
133
134
class Article(models.Model):
135
    AFRIKAANS = 'af'
136
    BELGIAN_FRENCH = 'fr-be'
137
    DUTCH = 'nl'
138
    ESPERANTO = 'eo'
139
    ENGLISH = 'en'
140
    FLEMISH = 'nl-be'
141
    FRENCH = 'fr'
142
    GERMAN = 'de'
143
    SPANISH = 'es'
144
145
    LANGUAGE_CODES = [
146
        (AFRIKAANS, 'Afrikaans'),
147
        (BELGIAN_FRENCH, 'Français (Belgique)'),
148
        (DUTCH, 'Nederlands'),
149
        (ESPERANTO, 'Esperanto'),
150
        (ENGLISH, 'English'),
151
        (FLEMISH, 'Vlaams'),
152
        (FRENCH, 'Français'),
153
        (GERMAN, 'Deutsch'),
154
        (SPANISH, 'Español')]
155
156
    visible = models.BooleanField(default=True)
157
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
158
    language_code = models.CharField(max_length=16,
159
                                      choices = LANGUAGE_CODES,
160
                                      blank=False)
161
    # file_path shouldn't be unique, because the same article file could be used
162
    # for multiple dialects of the same language.
163
    file_path = models.FilePathField(path=settings.MEDIA_ROOT + "blog/articles/",
164
                                     blank=False)
165
    # Same reason, slug shouldn't be unique
166
    slug = models.SlugField(unique=False, blank=False, allow_unicode=True)
167
    title = models.CharField(max_length=64, unique=False, blank=True)
168
169
    def text(self):
170
        return org_to_html(self.file_path, self.slug)
171
    def djhtml_file(self):
172
        return org_to_html(self.file_path, self.slug, return_djhtml_path=True)
173
    def headings(self):
174
        """ Returns the headings and their slugs present in this article. Useful
175
        for building the navigation drawer. """
176
        text = self.text()
177
        headers = list()
178
        lines = text.splitlines()
179
180
        for line in lines:
181
            # Heuristic approach to collecting headers
182
            if "<h" in line and "id=" in line and "</h" in line:
183
                first_part, second_part = line.split(">", maxsplit=1)
184
                slug = first_part.split('"')[-2]
185
                header = second_part.split("<", maxsplit=1)[0]
186
                headers.append((header, slug))
187
        return headers
188
189
190
191
192
class Comment(models.Model):
193
    """ Represents a comment on a blog post.
194
    Comments are not filtered by language; a
195
    comment made by someone reading the article in Dutch, that's written in
196
    Dutch, will show up (unedited) for somebody whom's reading the Spanish
197
    version.
198
    """
199
    # Allows me to manually hide certain messages if need be
200
    visible = models.BooleanField(default=True)
201
    date = models.DateTimeField(auto_now_add=True)
202
    name = models.CharField(max_length=64, blank=True)
203
    text = models.TextField(max_length=10000, blank=False)  # Should be more than enough
204
    # reaction_to is null if it's not a reaction to an existing comment
205
    reaction_to = models.ForeignKey('Comment', on_delete=models.CASCADE,
206
            null=True, blank=True)
207
    # blank=True is also needed per the Django documentation:
208
    # For both string-based and non-string-based fields, you will also need to
209
    # set blank=True if you wish to permit empty values in forms, as the null
210
    # parameter only affects database storage.
211
    from_myself = models.BooleanField(
212
        help_text="""I trust myself, so if I write a comment, I should be able
213
        to use the entire HTML5 suite. Ticking this box activates the DTL "safe"
214
        tag, instead of the "urlize" tag used for normal comments.""",
215
        default=False)
216
    post = models.ForeignKey(
217
        Post,
218
        on_delete=models.CASCADE,
219
        null=False,
220
        )
221
    class meta:
222
        ordering = ['date']  # When printed, prints the oldest comment first.
223
224
    def reactions(self):
225
        # Should return the comments that are a reaction to this comment
226
        return Comment.objects.filter(reaction_to=self).order_by('-date')
227
    def __str__(self):
228
        return str(self.id) +" | "+ self.name
229
    def is_new(self):
230
        # True if this comment was created less than one minute ago.
231
        now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC)
232
        delta = now-self.date
233
        return delta.seconds < 60
234
235
236
class FeedItem(models.Model):
237
    """ An item that shows up in the RSS feed."""
238
    title = models.CharField(max_length=64)
239
    added = models.DateTimeField(auto_now_add=True)
240
    description = models.CharField(max_length=400)
241
    link = models.URLField()
242