blog

Add comment functionality to blog posts

The different blog posts have now been fitted with a comment section below them. Normally all infrastructure is in place, including security, displaying, and error handling. Hyperlinks in comments are made clickable in the template by using the 'urlize' tag.

Author
Maarten 'Vngngdn' Vangeneugden
Date
April 9, 2018, 3:17 p.m.
Hash
692768670901c84f12ede4c3ffc2cf188704630e
Parent
fd2d13e18b0d7052ba31076d6122194a80c28ab2
Modified files
admin.py
forms.py
models.py
templates/blog/post.html
views.py

admin.py

1 addition and 1 deletion.

View changes Hide changes
1
1
2
2
from .models import Post
3
3
4
4
# Down here, all models are registered, making them suitable for administration (In essence; making them pop up in the admin interface).
5
-
admin.site.register(Post)
6
5
+
6

forms.py

14 additions and 0 deletions.

View changes Hide changes
+
1
from django.forms import ModelForm
+
2
from . import models
+
3
+
4
class CommentForm(ModelForm):
+
5
    class Meta:
+
6
        model = models.Comment
+
7
        fields = ['name',
+
8
                  'text',
+
9
                  'post',
+
10
                  ]
+
11
        widgets = {
+
12
            'post': None,
+
13
        }
+
14

models.py

7 additions and 6 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
import datetime
5
5
import os
6
6
7
7
def post_title_directory(instance, filename):
8
8
    """ Files will be uploaded to MEDIA_ROOT/blog/<year of publishing>/<blog
9
9
    title>
10
10
    The blog title is determined by the text before the first period (".") in
11
11
    the filename. So if the file has the name "Trains are bæ.en.md", the file
12
12
    will be stored in "blog/<this year>/Trains are bæ". Name your files
13
13
    properly!
14
14
    It should also be noted that all files are stored in the same folder if they
15
15
    belong to the same blogpost, regardless of language. The titles that are
16
16
    displayed to the user however, should be the titles of the files themselves,
17
17
    which should be in the native language. So if a blog post is titled
18
18
    "Universities of Belgium", its Dutch counterpart should be titled
19
19
    "Universiteiten van België", so the correct title can be derived from the
20
20
    filename.
21
21
22
22
    Recommended way to name the uploaded file: "<name of blog post in language
23
23
    it's written>.org". This removes the maximum amount of redundancy (e.g. the
24
24
    language of the file can be derived from the title, no ".fr.org" or something
25
25
    like that necessary), and can directly be used for the end user (the title
26
26
    is what should be displayed).
27
27
    """
28
28
    english_file_name = os.path.basename(instance.english_file.name) # TODO: Test if this returns the file name!
29
29
    english_title = english_file_name.rpartition(".")[0]
30
30
    year = datetime.date.today().year
31
31
32
32
    return "blog/{0}/{1}/{2}".format(year, english_title, filename)
33
33
34
34
class Post(models.Model):
35
35
    """ Represents a blog post. The title of the blog post is determnined by the name
36
36
    of the files.
37
37
    A blog post can be in 5 different languages: German, Spanish, English, French,
38
38
    and Dutch. For all these languages, a seperate field exists. Thus, a
39
39
    translated blog post has a seperate file for each translation, and is
40
40
    seperated from Django's internationalization/localization system.
41
41
    Only the English field is mandatory. The others may contain a value if a
42
42
    translated version exists, which will be displayed accordingly.
43
43
    """
44
44
    published = models.DateTimeField(auto_now_add=True)
45
45
    english_file = models.FileField(upload_to=post_title_directory, unique=True, blank=False)
46
46
    dutch_file = models.FileField(upload_to=post_title_directory, blank=True)
47
47
    french_file = models.FileField(upload_to=post_title_directory, blank=True)
48
48
    german_file = models.FileField(upload_to=post_title_directory, blank=True)
49
49
    spanish_file = models.FileField(upload_to=post_title_directory, blank=True)
50
50
    # Only the English file can be unique, because apparantly, there can't be
51
51
    # two blank fields in a unique column. Okay then.
52
52
53
53
    def __str__(self):
54
54
        return self.slug("en")
55
55
56
56
    def slug(self, language_code=translation.get_language()):
57
57
        """ Returns a slug of the requested language, or None if no version exists in that language. """
58
58
        possibilities = {
59
59
            "en" : self.english_file,
60
60
            "de" : self.german_file,
61
61
            "nl" : self.dutch_file,
62
62
            "fr" : self.french_file,
63
63
            "es" : self.spanish_file,
64
64
            }
65
65
        if possibilities[language_code]:
66
66
            return slugify(os.path.basename(possibilities[language_code].name).rpartition(".")[0])
67
67
        else:
68
68
            return None
69
69
70
70
#class Comment(models.Model):
71
-
    """ Represents a comment on a blog post.
+
71
    """ Represents a comment on a blog post.
72
72
73
73
    Comments are not linked to an account or anything, I'm trusting the
74
74
    commenter that he is honest with his credentials. That being said:
75
75
    XXX: Remember to put up a notification that comments are not checked for
76
76
    identity, and, unless verified by a trustworthy source, cannot be seen as
77
77
    being an actual statement from the commenter.
78
78
    Comments are linked to a blogpost, and are not filtered by language. (So a
79
79
    comment made by someone reading the article in Dutch, that's written in
80
80
    Dutch, will show up (unedited) for somebody whom's reading the Spanish
81
81
    version.
82
82
    XXX: Remember to notify (tiny footnote or something) that comments showing
83
83
    up in a foreign language is by design, and not a bug.
84
84
    """
85
85
    """date = models.DateTimeField(auto_now_add=True)
86
-
    name = models.CharField(max_length=64)
+
86
    name = models.CharField(max_length=64)
87
87
    text = models.TextField()
88
-
    post = models.ForeignKey(
+
88
    post = models.ForeignKey(
89
89
        Post,
90
90
        on_delete=Models.CASCADE,
91
91
        null=False) # TODO: Finish this class and the shit...
92
-
        """
93
-
        
94
-
+
92
        )
+
93
    class meta:
+
94
        ordering = ['date']  # When printed, prints the oldest comment first.
+
95

templates/blog/post.html

17 additions and 0 deletions.

View changes Hide changes
1
1
{% load i18n %}
+
2
{% load i18n %}
2
3
{% load static %}
3
4
4
5
{% block stylesheets %}
5
6
{{ block.super }}
6
7
<link href="{% static "blog/stylesheet.css" %}" rel="stylesheet" media="screen, projection" />
7
8
{% endblock %}
8
9
9
10
{% block description %}
10
11
{{ article|safe|truncatewords_html:20 }}
11
12
{% endblock description %}
12
13
{% block title %}📚 {{ navbar_title }}{% endblock title %}
13
14
14
15
15
16
{% block main %}
16
17
{% with color="brown" accent_color="yellow" %}
17
18
<div class="section {{ color }} lighten-1 z-depth-3">
18
19
    <div class="container">
19
20
        <article style="font-family:serif;">
20
21
            {{ article|safe }}
21
22
        </article>
22
23
23
24
24
25
<h5 class="white-text">{% trans "This article in other languages" %}</h5>
25
26
26
27
{% get_language_info for 'nl' as LANG %}
27
28
<a {% if dutch_link %} href="{{dutch_link}}" {% endif %}
28
29
   class="btn {{accent_color}} accent-4 black-text waves-effect
29
30
   {% if not dutch_link %}disabled{% endif %}">
30
31
    🇧🇪 {{ LANG.name_translated}} 🇳🇱
31
32
</a>
32
33
{% get_current_language as lang %}
33
34
{% get_language_info for 'fr' as LANG %}
34
35
<a {% if french_link %} href="{{french_link}}" {% endif %}
35
36
   class="btn {{accent_color}} accent-4 black-text waves-effect
36
37
   {% if not french_link %}disabled{% endif %}">
37
38
    🇧🇪 {{ LANG.name_translated}} 🇫🇷
38
39
</a>
39
40
{% get_language_info for 'en' as LANG %}
40
41
<a {% if english_link %} href="{{english_link}}" {% endif %}
41
42
   class="btn {{accent_color}} accent-4 black-text waves-effect
42
43
   {% if not english_link %}disabled{% endif %}">
43
44
    🇬🇧 {{ LANG.name_translated}} 🇺🇸
44
45
</a>
45
46
{% get_language_info for 'de' as LANG %}
46
47
<a {% if german_link %} href="{{german_link}}" {% endif %}
47
48
   class="btn {{accent_color}} accent-4 black-text waves-effect
48
49
   {% if not german_link %}disabled{% endif %}">
49
50
    🇧🇪 {{ LANG.name_translated}} 🇩🇪
50
51
</a>
51
52
{% get_language_info for 'es' as LANG %}
52
53
<a {% if spanish_link %} href="{{spanish_link}}" {% endif %}
53
54
   class="btn {{accent_color}} accent-4 black-text waves-effect
54
55
   {% if not spanish_link %}disabled{% endif %}">
55
56
    🇪🇸 {{ LANG.name_translated}} 🇲🇽
56
57
</a>
57
58
{% comment %}
+
59
<h5 class="white-text">{% trans "Comments" %}</h5>
+
60
{% for comment in comments %} {# Whoops =P #}
+
61
    <span class="white-text">{{ comment.name|title }} | </span>
+
62
    <time class="grey-text" datetime="{{ comment.date|date:'c' }}">{{ comment.date|naturaltime }}</time>
+
63
    <br />
+
64
    <p class="white-text">{{ comment.text|urlize }}</p>
+
65
    <hr />
+
66
{% endfor %}
+
67
    {# Form for new comment #}
+
68
    <form action="" method="POST">
+
69
        {{ form }}
+
70
        <input type="submit" value="{% trans "Submit" %}" />
+
71
    </form>
+
72
+
73
+
74
{% comment %}
58
75
<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>
59
76
    {# TODO: Change to rainbow flag when possible #}
60
77
{% endcomment %}
61
78
    </div>
62
79
63
80
</div>
64
81
<div class="container">
65
82
    {% for title, date, description, link in post_links %}
66
83
        <h2 class="{{ color}}-text">{{ title }}</h2>
67
84
        {# FIXME: Date is in all languages of the same format. Fix for each language #}
68
85
        <span class="grey-text">{{ date|date:"l j F Y" }}</span>
69
86
        {#<p class="hide-on-small-only">{{ description }}</p>#}
70
87
        <p class="hide-on-small-only">{% lorem %}</p>
71
88
        <a class="btn {{accent_color}} accent-3" href="{{link}}">
72
89
            {% trans "Read on"%}
73
90
        </a>
74
91
        <hr />
75
92
    {% endfor %}
76
93
</div>
77
94
{% endwith %}
78
95
{% endblock main %}
79
96

views.py

9 additions and 0 deletions.

View changes Hide changes
1
1
import subprocess
2
2
3
3
from django.utils.translation import ugettext as _
4
4
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.
5
5
from django.http import HttpResponseRedirect, HttpResponse
6
6
from django.core.urlresolvers import reverse # Why?
7
7
from django.template import loader # This allows to actually load the template.
8
8
from django.contrib.auth.decorators import login_required
9
9
from django.contrib.auth import authenticate, login
10
10
from .models import Post
11
11
from django.core.exceptions import ObjectDoesNotExist
+
12
from django.core.exceptions import ObjectDoesNotExist
12
13
from django.utils import translation
13
14
14
15
GERMAN = "de"
15
16
SPANISH = "es"
16
17
FRENCH = "fr"
17
18
DUTCH = "nl"
18
19
ENGLISH = "en"
19
20
20
21
footer_links = [
21
22
        [_("Blog main page"), "/blog"],
22
23
        [_("Contact"), "mailto:maarten.vangeneugden@student.uhasselt.be"],
23
24
        ]
24
25
footer_description = _("Maarten's personal blog, with sprinkles and a dollop of healthy bugs.")
25
26
26
27
# FIXME: Remove this template trash. THIS IS A VIEW, NOT A FUCKING TEMPLATE FFS
27
28
context = {
28
29
    'materialDesign_color': "green",
29
30
    'materialDesign_accentColor': "purple",
30
31
    'navbar_title': "Blog",
31
32
    'navbar_fixed': True,
32
33
    'navbar_backArrow': True,
33
34
    #'footer_title': "Maarten's blog",
34
35
    #'footer_description': "My personal scribbly notepad.",
35
36
    #'footer_links': footer_links,
36
37
    }
37
38
38
39
def org_to_html(file_path):
39
40
    """ Converts the given org formatted file to HTML.
40
41
    This function directly returns the resulting HTML code. This function uses
41
42
    the amazing Haskell library Pandoc to convert the file (and takes care
42
43
    of header id's and all that stuff).
43
44
    """
44
45
    # FIXME: Remove hardcoded link to media. Replace with media tag!
45
46
    return subprocess.check_output(["pandoc", "--from=org", "--to=html", "/srv/django/website/media/"+file_path])
46
47
47
48
def get_available_post_languages(post):
48
49
    """ Returns the language codes for which a blog post exists. This function
49
50
    always returns English (because that field mustn't be empty).
50
51
    So say a blog post has an English, Dutch and French version (which means
51
52
    english_file, french_file and dutch_file aren't empty), the function will return {"en",
52
53
    "fr", "nl"}. """
53
54
    available_languages = {ENGLISH}
54
55
    if post.german_file != "":
55
56
        available_languages.add(GERMAN)
56
57
    if post.spanish_file != "":
57
58
        available_languages.add(SPANISH)
58
59
    if post.french_file != "":
59
60
        available_languages.add(FRENCH)
60
61
    if post.dutch_file != "":
61
62
        available_languages.add(DUTCH)
62
63
    return available_languages
63
64
64
65
def get_preferred_post_language(post, language):
65
66
    """ Returns the post language file that best suits the given language. This
66
67
    is handy if you know what language the user prefers, but aren't sure whether
67
68
    you can provide that language. This function will try to provide the file
68
69
    for that language, or return English if that's not possible. """
69
70
    if language == GERMAN and post.german_file is not None:
70
71
        return post.german_file
71
72
    if language == SPANISH and post.spanish_file is not None:
72
73
        return post.spanish_file
73
74
    if language == FRENCH and post.french_file is not None:
74
75
        return post.french_file
75
76
    if language == DUTCH and post.dutch_file is not None:
76
77
        return post.dutch_file
77
78
    return post.english_file  # Returned if all other choices wouldn't be satisfactory, or the requested language is English.
78
79
79
80
def index(request):
80
81
    template = "blog/index.html"
81
82
    posts = Post.objects.all()
82
83
    language = translation.get_language()
83
84
84
85
    post_links = []
85
86
    for post in posts:
86
87
        blog_file = get_preferred_post_language(post, language)
87
88
        # TODO: Find a cleaner way to determine the title. First and foremost:
88
89
        # If the language differs from English, the other language file needs to
89
90
        # be loaded. Plus: look for a built in function to remove the full path
90
91
        # and only return the file name.
91
92
        title = (blog_file.name.rpartition("/")[2]).rpartition(".")[0]
92
93
        human_title = title.replace("_"," ")
93
94
        date = post.published
94
95
        blog_text = org_to_html(blog_file.name)
95
96
        # TODO: The link can possibly be reversed in the DTL using the title, which is actually
96
97
        # a cleaner way to do it. Investigate.
97
98
        link = reverse("blog-post-language", args=[language, post.slug(language)])
98
99
        post_links.append([human_title, date, blog_text, link])
99
100
100
101
    context = {
101
102
            'posts': post_links,
102
103
            'materialDesign_color': "brown",
103
104
            'materialDesign_accentColor': "yellow",
104
105
            'navbar_title': _("Notepad from a student"),
105
106
            'navbar_backArrow': True,
106
107
            'footer_links': footer_links,
107
108
            'footer_description': footer_description,
108
109
            }
109
110
    return render(request, template, context)
110
111
111
112
def post(request, post_slug, language=None):
112
113
    if language is not None:
+
114
        form = CommentForm(request.POST)
+
115
        if form.is_valid():
+
116
            form.save()
+
117
+
118
    if language is not None:
113
119
        if translation.check_for_language(language):
114
120
            translation.activate(language)
115
121
            request.session[translation.LANGUAGE_SESSION_KEY] = language
116
122
            #return post(request, post_slug)
117
123
    else:
118
124
        language = translation.get_language()
119
125
120
126
    template = "blog/post.html"
121
127
    posts = Post.objects.all()
122
128
    #comments = Comment.objects.filter(post
123
129
    for post in posts:
124
130
        if post.slug(language) == post_slug:
125
131
            blog_file = get_preferred_post_language(post, language)
+
132
            form = CommentForm(initial={'post': post})
+
133
            blog_file = get_preferred_post_language(post, language)
126
134
            blog_text = org_to_html(blog_file.name)
127
135
            context = {
128
136
                'human_post_title': blog_file.name.replace("_"," "),
+
137
                'human_post_title': blog_file.name.replace("_"," "),
129
138
                'materialDesign_color': "brown",
130
139
                'materialDesign_accentColor': "yellow",
131
140
                'article': blog_text,
132
141
                'title': str(blog_file),
133
142
                'navbar_title': ((blog_file.name.rpartition("/")[2]).rpartition(".")[0]).replace("_"," "),
134
143
                'navbar_backArrow': False,
135
144
                'post_slug': post_slug,
136
145
                'footer_links': footer_links,
137
146
                'footer_description': footer_description,
138
147
                }
139
148
140
149
            # Getting all available article links
141
150
            available = get_available_post_languages(post)
142
151
            if ENGLISH in available:
143
152
                context['english_link'] = reverse("blog-post-language", args=[ENGLISH, post.slug(ENGLISH)])
144
153
            if DUTCH in available:
145
154
                context['dutch_link'] = reverse("blog-post-language", args=[DUTCH, post.slug(DUTCH)])
146
155
147
156
            if FRENCH in available:
148
157
                context['french_link'] = reverse("blog-post-language", args=[FRENCH, post.slug(FRENCH)])
149
158
150
159
            if SPANISH in available:
151
160
                context['spanish_link'] = reverse("blog-post-language", args=[SPANISH, post.slug(SPANISH)])
152
161
153
162
            if GERMAN in available:
154
163
                context['german_link'] = reverse("blog-post-language", args=[GERMAN, post.slug(GERMAN)])
155
164
156
165
            return render(request, template, context)
157
166