models.py
1 |
|
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 |