blog

Numerous improvements to blog app

- Added a new method to Comment, telling whether the comment is brand new or not. - Fix the order in which comments appear below a given article. - When submitting a comment, a snackbar now pops up informing the user whether submission was succesful. - Changes to the presentation in the templates.

Author
Maarten Vangeneugden
Date
April 3, 2022, 7:33 p.m.
Hash
226361afe87e17f982d6457e1e83d315faf0fd18
Parent
c20135034fa1e7ba77d5b15c18b947742c03f1aa
Modified files
models.py
templates/blog/comment.djhtml
templates/blog/post.djhtml
views.py

models.py

7 additions and 0 deletions.

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

templates/blog/comment.djhtml

17 additions and 5 deletions.

View changes Hide changes
1
1
{% load humanize %}
2
2
3
3
<div class="comment" id="reago-{{ comment.id }}">
+
4
{# Check if this comment was added just now #}
+
5
{% if comment.is_new %}
+
6
<div class="comment recently-posted" id="reago-{{ comment.id }}">
+
7
{% else %}
+
8
<div class="comment" id="reago-{{ comment.id }}">
4
9
    <p><a href="#reago-{{ comment.id }}">#{{ comment.id }}</a>
+
10
+
11
+
12
<!--<div class="comment recently-posted" id="reago-{{ comment.id }}">-->
+
13
    <p><a href="#reago-{{ comment.id }}">#{{ comment.id }}</a>
5
14
        {{ comment.name }} |
6
15
        {{ comment.date|naturaltime }} 
7
16
       ({{ comment.date|date:"SHORT_DATE_FORMAT" }})
8
-
    </p>
+
17
    </p>
9
18
    <p>{{ comment.text|urlize }}</p>
10
19
    <details>
11
20
        <summary>{% translate "Respond" %}</summary>
12
-
        <form method="POST">
13
-
            {% csrf_token %}
+
21
        {# Doing action="#reago-{{ comment.id }}" is a clever hack if I say so #}
+
22
        {# myself; it makes sure that, after submission, the user is immediately #}
+
23
        {# redirected to the comment he responded to, instead of going to the #}
+
24
        {# top again. Pretty neat huh? =D #}
+
25
        <form method="POST" action="#reago-{{ comment.id }}">
+
26
            {% csrf_token %}
14
27
            <input type="hidden" name="reaction_to" value="{{ comment.id }}">
15
28
            <input type="hidden" name="post" value="{{ article.post.id }}">
16
29
            <input type="text" id="name-{{ comment.id }}" name="name" maxlength="64" required>
17
30
            <label for="name-{{ comment.id }}">{% translate "Your name" %}</label><br>
18
31
            <textarea name="text" id="text-{{ comment.id }}" maxlength="10000" required></textarea>
19
-
            <label for="text-{{ comment.id }}">{% translate "Your comment" %}</label><br>
20
-
            <input type="submit" value="{% translate "Submit" %}">
+
32
            <input type="submit" value="{% translate "Submit" %}">
21
33
        </form>
22
34
    </details>
23
35
    {% for subcomment in comment.reactions %}
24
36
        {% include "blog/comment.djhtml" with comment=subcomment %}
25
37
    {% endfor %}
26
38
</div>
27
39

templates/blog/post.djhtml

20 additions and 5 deletions.

View changes Hide changes
1
1
{% load humanize %}
2
2
{% load i18n %}
3
3
{% load static %}
4
4
5
5
6
6
{% block stylesheets %}
7
7
{{ block.super }}
8
8
<style>
9
9
@font-face {
10
10
  font-family: 'Merriweather';
11
11
  font-style: italic;
12
12
  font-weight: 400;
13
13
  src: url({% get_static_prefix %}fonts/merriweather-400-italic.woff2) format('woff2');
14
14
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
15
15
}
16
16
@font-face {
17
17
  font-family: 'Merriweather';
18
18
  font-style: normal;
19
19
  font-weight: 400;
20
20
  src: url({% get_static_prefix %}fonts/merriweather-400-regular.woff2) format('woff2');
21
21
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
22
22
}
23
23
@font-face {
24
24
  font-family: 'Merriweather';
25
25
  font-style: normal;
26
26
  font-weight: 700;
27
27
  src: url({% get_static_prefix %}fonts/merriweather-700-regular.woff2) format('woff2');
28
28
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
29
29
}
30
30
31
31
body {
32
32
    background-color: #0d1521; /*#0d47a1; /* Material blue P900 */
33
33
}
34
34
body video {
35
35
    width: 80%;
36
36
}
37
37
header {
38
38
    background-color: #3e2723;
39
39
}
40
40
section.article {
41
41
    background-color: #efebe9;/*rgb(210, 188, 157);*/
42
42
    font-family: Merriweather, serif;
43
43
}
44
44
45
45
.comment {
46
46
    margin-left: 1em;
47
-
    padding-left: 1em;
48
-
    border-left-style: solid;
+
47
    padding-left: 0.5em;
+
48
    padding-bottom: 0.5em;
+
49
    border-left-style: solid;
49
50
    border-color: var(--primary);
50
51
}
51
52
</style>
+
53
    background-color: var(--P100);
+
54
}
+
55
+
56
span.small-detail {
+
57
    font-size: smaller;
+
58
    color: grey;
+
59
}
+
60
</style>
52
61
{% endblock stylesheets %}
53
62
54
63
{% block description %}
55
64
{{ article.text|safe|truncatewords_html:10 }}
56
65
{% endblock description %}
57
66
{% block title %}📚 {{ navbar_title }}{% endblock title %}
58
67
59
68
60
69
{% block header %}
61
70
<header>
62
71
<h1>{{ navbar_title }}</h1>
63
72
</header>
64
73
{% endblock header %}
65
74
{% block main %}
66
75
<section class="article">
67
76
    <!--<article style="font-family:serif;">-->
68
77
    {#{{ article.text|safe }}#}
69
78
    {% include "/tmp/blog-file.djhtml" %}
70
79
71
80
    <!--</article>-->
72
81
</section>
73
82
<section class="reagoj">
+
83
{% if comment_response %}
+
84
<div class="snackbar">
+
85
    {{ comment_response }}
+
86
</div>
+
87
{% endif %}
+
88
+
89
<section class="reagoj">
74
90
    <h2>{% translate "Comments" %}</h2>
75
91
    <form method="POST">
76
-
        {% csrf_token %}
+
92
        {% csrf_token %}
77
93
        <input type="hidden" name="reaction_to" value="">
78
94
        <input type="hidden" name="post" value="{{ article.post.id }}">
79
95
        <input type="text" id="name-root" name="name" maxlength="64" required>
80
96
        <label for="name-root">{% translate "Your name" %}</label><br>
81
97
        <textarea name="text" id="text-root" maxlength="10000" required></textarea>
82
-
        <label for="text-root">{% translate "Your comment" %}</label><br>
83
-
        <input type="submit" value="{% translate "Submit" %}">
+
98
        <input type="submit" value="{% translate "Submit" %}">
84
99
    </form>
85
100
    <hr>
86
101
    {% for root_comment in root_comments %}
87
102
        {% include "blog/comment.djhtml" with comment=root_comment %}
88
103
    {% endfor %}
89
104
</section>
90
105
91
106
{% comment %}
92
107
<h5 class="white-text">{% trans "This article in other languages" %}</h5>
93
108
94
109
{% get_language_info for 'nl' as LANG %}
95
110
<a {% if dutch_link %} href="{{dutch_link}}" {% endif %}
96
111
   class="btn fill
97
112
   {% if not dutch_link %}disabled{% endif %}">
98
113
    🇧🇪 {{ LANG.name_translated}} 🇳🇱
99
114
</a>
100
115
{% get_current_language as lang %}
101
116
{% get_language_info for 'fr' as LANG %}
102
117
<a {% if french_link %} href="{{french_link}}" {% endif %}
103
118
   class="btn fill
104
119
   {% if not french_link %}disabled{% endif %}">
105
120
    🇧🇪 {{ LANG.name_translated}} 🇫🇷
106
121
</a>
107
122
{% get_language_info for 'en' as LANG %}
108
123
<a {% if english_link %} href="{{english_link}}" {% endif %}
109
124
   class="btn fill
110
125
   {% if not english_link %}disabled{% endif %}">
111
126
    🇬🇧 {{ LANG.name_translated}} 🇺🇸
112
127
</a>
113
128
{% get_language_info for 'de' as LANG %}
114
129
<a {% if german_link %} href="{{german_link}}" {% endif %}
115
130
   class="btn fill
116
131
   {% if not german_link %}disabled{% endif %}">
117
132
    🇧🇪 {{ LANG.name_translated}} 🇩🇪
118
133
</a>
119
134
{% get_language_info for 'es' as LANG %}
120
135
<a {% if spanish_link %} href="{{spanish_link}}" {% endif %}
121
136
   class="btn
122
137
   {% if not spanish_link %}disabled{% endif %}">
123
138
    🇪🇸 {{ LANG.name_translated}} 🇲🇽
124
139
</a>
125
140
{% endcomment %}
126
141
127
142
{% comment %}
128
143
<a href="{% url 'blog-post' post_slug %}" class="btn {{accent_color}} accent-4 black-text tooltipped" data-position="bottom" data-delay="50" data-tooltip="{% trans "Multilingual link. Links to the version in the viewer's preferred language." %}">🏳️‍🌈 {% trans "All available languages" %}</a>
129
144
    {# TODO: Change to rainbow flag when possible #}
130
145
{% endcomment %}
131
146
    </div>
132
147
133
148
</div>
134
149
{% endblock main %}
135
150

views.py

5 additions and 2 deletions.

View changes Hide changes
1
1
import requests
2
2
3
3
4
4
from django.utils.translation import ugettext as _
5
5
from django.shortcuts import get_object_or_404, render # This allows to render the template with the view here. It's pretty cool and important.
6
6
from django.http import HttpResponseRedirect, HttpResponse
7
7
from django.urls import reverse
8
8
from django.template import loader # This allows to actually load the template.
9
9
from .models import *
10
10
from .forms import CommentForm
11
11
from django.core.exceptions import ObjectDoesNotExist
12
12
from django.utils import translation
13
13
14
14
GERMAN = "de"
15
15
SPANISH = "es"
16
16
FRENCH = "fr"
17
17
DUTCH = "nl"
18
18
ENGLISH = "en"
19
19
20
20
def index(request):
21
21
    template = "blog/index.djhtml"
22
22
    posts = Post.objects.exclude(visible=False)
23
23
24
24
25
25
    context = {
26
26
            'posts': posts,
27
27
            'navbar_title': _("Notepad from a student"),
28
28
            'navbar_backArrow': True,
29
29
            'stylesheet_name': "blog",
30
30
            }
31
31
    return render(request, template, context)
32
32
33
33
def post(request, language_code, post_slug):
34
34
    if request.method == "POST":  # Handling a reply if one is sent
+
35
    if request.method == "POST":  # Handling a reply if one is sent
35
36
        form = CommentForm(request.POST)
36
37
37
38
        form.post = Post.objects.get(id=request.POST['post'])
38
39
        if form.is_valid():
39
40
            new_comment = form.save(commit=False)
40
41
            if request.POST['reaction_to'] != "":
41
42
                new_comment.reaction_to = Comment.objects.get(id=request.POST['reaction_to'])
42
43
            new_comment.save()
43
44
        else:
+
45
        else:
44
46
            print("ERROR")
45
47
            print(form.errors)
46
48
+
49
47
50
    template = "blog/post.djhtml"
48
51
    article = Article.objects.get(slug=post_slug, language_code=language_code)
49
52
    root_comments = Comment.objects.filter(post=article.post, reaction_to=None)
50
-
    context = {
51
-
            'article': article,
+
53
    context = context | {
+
54
            'article': article,
52
55
            'root_comments': root_comments,
53
56
            'title': article.post.title(),
54
57
            'navbar_title': article.post.title(),
55
58
            'navbar_backArrow': True,
56
59
            'stylesheet_name': "blog"}
57
60
58
61
    return render(request, template, context)
59
62
60
63
def rss(request):
61
64
    template = "blog/feed.rss"
62
65
    context = {
63
66
        'items': FeedItem.objects.all(),
64
67
        }
65
68
    return render(request, template, context, content_type="application/rss+xml")
66
69
67
70
68
71
def archive(request):
69
72
    template = "blog/monthly_archive.djhtml"
70
73
    language = translation.get_language()
71
74
72
75
    file_2017 = org_to_html("blog/weekly/2017.org")
73
76
    file_2018 = org_to_html("blog/weekly/2018.org")
74
77
    file_2019 = org_to_html("blog/weekly/2019.org")
75
78
76
79
77
80
78
81
    context = {
79
82
        't2017': file_2017,
80
83
        't2018': file_2018,
81
84
        't2019': file_2019,
82
85
            'materialDesign_color': "brown",
83
86
            'materialDesign_accentColor': "blue",
84
87
            'navbar_title': _("Weekly-archief"),
85
88
            'navbar_backArrow': True,
86
89
            'footer_links': footer_links,
87
90
            'footer_description': footer_description,
88
91
            'stylesheet_name': "blog",
89
92
            }
90
93
    return render(request, template, context)
91
94