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