first commit
This commit is contained in:
commit
8aee826f7d
12 changed files with 361 additions and 0 deletions
22
LICENSE
Normal file
22
LICENSE
Normal file
|
@ -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.
|
30
README.md
Normal file
30
README.md
Normal file
|
@ -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.
|
0
__init__.py
Normal file
0
__init__.py
Normal file
6
admin.py
Normal file
6
admin.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import Thread, Comment
|
||||||
|
|
||||||
|
admin.site.register(Thread)
|
||||||
|
admin.site.register(Comment)
|
5
apps.py
Normal file
5
apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CommentsConfig(AppConfig):
|
||||||
|
name = 'comments'
|
51
ext.py
Normal file
51
ext.py
Normal file
|
@ -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:])
|
22
models.py
Normal file
22
models.py
Normal file
|
@ -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
|
68
static/thread.css
Normal file
68
static/thread.css
Normal file
|
@ -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;
|
||||||
|
}
|
43
templates/comments/thread.html
Normal file
43
templates/comments/thread.html
Normal file
|
@ -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>
|
3
tests.py
Normal file
3
tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
9
urls.py
Normal file
9
urls.py
Normal file
|
@ -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'),
|
||||||
|
]
|
102
views.py
Normal file
102
views.py
Normal file
|
@ -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,)))
|
Loading…
Reference in a new issue