all repos — comments @ 8aee826f7ddc03fd20cadfe2a4fab5f521fdbc04

django app for embedding comment threads in a static site via iframes

first commit
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQJDBAABCAAtFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAl6otb8PHG5pbGl4QG5p
bGZtLmNjAAoJEDt/vCIUTmOYVhMP/1OfpqWDoC/9za6CX5zXlhp2moeq7F18TBcC
74y3ov8P88XBGhcCt61tRIcwQlzvQG6zjif+5eVIt+jW7psI6bKEsi4t44m+ixWU
7Ix5jXtq8V1duuhuu1GRP66E4dyrkXqCNuYKxjhyw4oxZINcVJ8ci+6NntryCSY+
MIN/U0R3imBiaVcelz5b0i8xaULopu00AEzEPSHornK1tzysJxDzfqsr1S8lQ8wP
P3/MZEG7yTba0jKgZgYlEIWa33kXI817bnnRKtEgGZYCIUpxhEe5SsRVqHBcOEER
NNK44vfz2S4YM7lPuDUakWjT9eDlu/yg1qGG9b9YdZux3QIZnTS/g+kD/xmhbhHV
E0Y6AJlCdigFwfsGb8zUNYj8nkGV9ljJ9MlQkDlH17N2VUJdgiUnFgebuqr6NBzX
Hd1Ecwvj0sXlzf6dc8e+LUEbwSsK8fThL3anosLKy43Aj8zOGY7kzH5tpVwlZzWB
PP+yyuCc7o8eVDtYXoK+l+ThsYigNpjRzgwsPCcL5xQCr5p8i1DLTV6/Wp7C6p4w
LkPdBd5CkjXDRsLylXbwA7Rb7Jg8QLcJ6htP+wL15GX/SGGrHm1f+cwpWIcrS8v2
DbXG9iDuAJHaKAQAJzzbWOXPU68KZzPUBNmOfBbyy47xDqG6tiDTb4HvXqp2D4Hp
dMTHtCJL
=UPCt
-----END PGP SIGNATURE-----
commit

8aee826f7ddc03fd20cadfe2a4fab5f521fdbc04

A LICENSE

@@ -0,0 +1,22 @@

+Copyright (c) 2020, Derek Stevens +drkste@zoho.com + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
A README.md

@@ -0,0 +1,30 @@

+# [[ comments ]] +### - a simple embeddable comment system for Django - + +## about +`comments` is an unceremoniously named comment system created with the Django framework with the intention of adding comment capabilities to an otherwise statically generated site. Together with my `ayanami` CMS this provides a featureful and lightweight web platform for content generation, sharing, and discussion. + +## usage +`comments` is a Django app. Thus, to use it, drop the `comments` directory into your django project directory, include the `urls.py` into the global one, and probably copy or move the `ext.py` to the django global directory, editing it to match your djang project's name. + +Once your django setup is done, you can call `ext.py create *` from your static site generator to create comment threads, and embed iframes pointing to `/your/django/dir/comments/thread` in your page. To manage comments, you can use the vanilla django admin console. You can start at a thread, and from the `root_comment` through each `next` comment you can use the `change` directive to follow the comment thread to the comment of interest; or you can manage by comment directly. Comments can be hidden to avoid manually the relinking the threads after actually deleting them. + +## data + +Comments are stored in a linked list. + +Each thread is just: +* `thread_id`: a unique identifier (primary key) for the thread +* `root_comment`: the first comment in the thread; `None` if empty + +And each comment is structured as: +* `comment_author` +* `comment_author_email`: this is only used internally for accountability reasons +* `comment_date`: this is automatically generated when the comment is created +* `hidden`: a boolean flag whether to show the comment or not +* `comment_data`: the textual content of the comment +* `next`: the next comment in the thread; `None` if the last + +## licensing + +`comments` is released under a 2-clause BSD License (`LICENSE` file). Use it however you want as long as you reproduce the `LICENSE` in the distribution and allow access to the source.
A admin.py

@@ -0,0 +1,6 @@

+from django.contrib import admin + +from .models import Thread, Comment + +admin.site.register(Thread) +admin.site.register(Comment)
A apps.py

@@ -0,0 +1,5 @@

+from django.apps import AppConfig + + +class CommentsConfig(AppConfig): + name = 'comments'
A ext.py

@@ -0,0 +1,51 @@

+# comments/ext.py +# (c) 2020 Derek Stevens <drkste@zoho.com> + +# this is a helper script to initialize comment threads externally +# move this to the project directory and change the settings imports accordingly + +import sys +from django.conf import settings +import nilfm.settings as nilfm_settings + +settings.configure(INSTALLED_APPS=nilfm_settings.INSTALLED_APPS, DATABASES=nilfm_settings.DATABASES) + +import django +django.setup() + +from comments.models import Comment, Thread + +def echo(*args): + threads = Thread.objects.all(); + for t in threads: + print(t) + c = t.root_comment + print(c) + if c: + c = c.next + print(c) + +def create(id): + x = Thread(thread_id=id, ) + x.save() + +def postTo(**kwargs): + id = kwargs["id"] + name = kwargs["name"] + mail = kwargs["mail"] + data = kwargs["data"] + t = Thread.objects.get(pk=id) + current = t.root_comment + if current: + while current: + current = current.next + current = Comment(comment_author=name, comment_author_email=mail, comment_data=data) + current.save() + +options = { + "echo": echo, + "create": create, + "postTo": postTo + } + +options[sys.argv[1]](sys.argv[2:])
A models.py

@@ -0,0 +1,22 @@

+# comments/models.py +# (c) 2020 Derek Stevens <drkste@zoho.com> + +from django.db import models +from datetime import datetime + +class Comment(models.Model): + comment_author = models.CharField(max_length=128, blank=False) + comment_author_email = models.CharField(max_length=128, blank=False) + comment_date = models.DateTimeField(default=datetime.now, blank=True) + comment_data = models.CharField(max_length=4096, blank=False) + hidden = models.BooleanField(default=False) + next = models.ForeignKey('self', on_delete=models.SET_NULL, null=True) + def __str__(self): + return self.comment_author + " <" + self.comment_author_email + "> @" + self.comment_date.strftime('%Y-%m-%d %H:%M') + ": " + self.comment_data + + +class Thread(models.Model): + thread_id = models.CharField(primary_key=True, max_length=64) + root_comment = models.ForeignKey(Comment, on_delete=models.SET_NULL, null=True) + def __str__(self): + return self.thread_id
A static/thread.css

@@ -0,0 +1,68 @@

+body +{ + font-family: Monospace; + font-size: 10px; + color: #797979; + background-color: #000000; +} + +#main +{ +} + +#commentwrapper +{ + position: relative; + display: grid; + grid-template-rows: 18px 18px 1fr; + grid-template-columns: 1fr; +} + +.author +{ + color: #c4c4c4; + grid-row: 1; + padding-top: 8px; +} + +.datetime +{ + color: #3f3f3f; + grid-row: 2; + padding-left: 4px; +} + +.commentdata +{ + grid-row: 3; + padding-left: 4px; +} + +#errormsg +{ + color: #c43f3f +} + +.myInputs +{ + border: 1px solid #3f3f3f; +} + +.myButton +{ + font-weight: bold; + color: #3b9088; + background-color: #000000; + border: none; +} + +.myButton:hover +{ + color: #6aa6a0; +} + +textarea +{ + width: 100%; + height: 48px; +}
A templates/comments/thread.html

@@ -0,0 +1,43 @@

+{% load static %} +<html> + <head> + <meta charset="utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + <meta name="description" content="lair of nilix" /> + <meta name="HandheldFriendly" content="True" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>comments for {{ thread.thread_id }}</title> + <link rel="shortcut icon" href="/favicon.ico"> + <link rel="stylesheet" type="text/css" href="{% static "thread.css" %} "> + </head> + <body> + <div id="main"> + {% for comment in comments %} + <div class="commentwrapper"> + <div class="author">{{ comment.comment_author }}</div> + <div class="datetime">{{ comment.comment_date }}</div> + <div class="commentdata">{{ comment.comment_data }}</div> + </div> + {% empty %} + <p>No comments yet!</p> + {% endfor %} + + <p> Post a comment</p> + {% if error_message %} + <b id="errormsg"> {{ error_message }} </b> + {% endif %} + <div class="formwrapper"> + <form action = "{% url 'comments:post' thread.thread_id %}" method="POST"> + {% csrf_token %} + name:<br/> + <input type="text" name="comment_author" class="myInputs" maxlength=128/><br/> + email:<br/> + <input type="text" name="comment_author_email" class="myInputs" maxlength=128/><br/> + comment:<br/> + <textarea name="comment_data" class="myInputs" rows="10" cols="70" wrap="hard"></textarea><br/> + <input type="submit" value="Post" class="myButton"/> + </form><br/> + </div> + </div> + </body> +</html>
A tests.py

@@ -0,0 +1,3 @@

+from django.test import TestCase + +# Create your tests here.
A urls.py

@@ -0,0 +1,9 @@

+from django.urls import path +from . import views + +app_name = "comments" + +urlpatterns = [ + path('<str:thread_id>/', views.thread, name='thread'), + path('<str:thread_id>/post/', views.post, name='post'), + ]
A views.py

@@ -0,0 +1,102 @@

+# Comments/views.py +# (c) 2020 Derek Stevens <drkste@zoho.com> + +from django.shortcuts import render +from django.http import HttpResponse, HttpResponseRedirect +from .models import Comment, Thread +from django.template import loader +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.core.validators import ValidationError +from django.views.decorators.clickjacking import xframe_options_sameorigin + +def buildCommentList(cThread): + cList = None + if cThread and cThread.root_comment: + current = cThread.root_comment + if not current.hidden: + cList = [ current ] + while current.next: + current = current.next + if not current.hidden: + if cList: + cList.append(current) + else: + cList = [ current ] + return cList + +@xframe_options_sameorigin +def thread(request, thread_id): + cThread = get_object_or_404(Thread, pk=thread_id) + + commentList = buildCommentList(cThread) + + template = loader.get_template('comments/thread.html') + context = { 'thread': cThread, 'comments': commentList } + return HttpResponse(template.render(context, request)) + +def checkMailAddr(addr): + if "@" in addr: + if addr[0] == "@": + raise ValidationError("Invalid email address!") + + domain = addr.split("@")[1] + if "." in domain and len(domain) >= 5: + for i in domain.split("."): + if len(i) < 2: + raise ValidationError("Invalid email address!") + return 1 + + else: + raise ValidationError("Invalid email address!") + else: + raise ValidationError("Invalid email address!") + +def checkLength(name, x): + if len(name) > x: + return 1 + else: + raise ValidationError("Not enough characters in field!") + +@xframe_options_sameorigin +def post(request, thread_id): + cThread = get_object_or_404(Thread, pk=thread_id) + template = loader.get_template('comments/thread.html') + + commentList = buildCommentList(cThread) + + context = {'thread': cThread, 'comments': commentList} + if request.POST: + name = request.POST['comment_author'] + mail = request.POST['comment_author_email'] + data = request.POST['comment_data'] + + try: + validationCounter = 0 + validationCounter += checkLength(name, 1) + validationCounter += checkMailAddr(mail) + validationCounter += checkLength(data, 8) + except ValidationError: + if validationCounter == 0: + context['error_message'] = "What was your name again?" + if validationCounter == 1: + context['error_message'] = "Enter a valid e-mail address, please. It is only recorded for accountability; it is not publicized." + if validationCounter == 2: + context['error_message'] = "Say something meaningful! At least 8 characters are required for the comment field." + return HttpResponse(template.render(context, request)) + + newComment = Comment(comment_author=name, comment_author_email=mail, comment_data=data) + newComment.save() + if cThread.root_comment: + c = cThread.root_comment + while c: + last = c + c = c.next + last.next = newComment + last.save() + else: + cThread.root_comment = newComment + + cThread.save() + + return HttpResponseRedirect(reverse('comments:thread', args=(thread_id,)))