aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDerek Stevens <nilix@nilfm.cc>2020-04-28 19:01:19 -0400
committerDerek Stevens <nilix@nilfm.cc>2020-04-28 19:01:19 -0400
commit8aee826f7ddc03fd20cadfe2a4fab5f521fdbc04 (patch)
tree910b5fb98be422821eadeedde6932d2772279912
first commit
-rw-r--r--LICENSE22
-rw-r--r--README.md30
-rw-r--r--__init__.py0
-rw-r--r--admin.py6
-rw-r--r--apps.py5
-rw-r--r--ext.py51
-rw-r--r--models.py22
-rw-r--r--static/thread.css68
-rw-r--r--templates/comments/thread.html43
-rw-r--r--tests.py3
-rw-r--r--urls.py9
-rw-r--r--views.py102
12 files changed, 361 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..385fb38
--- /dev/null
+++ b/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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7cb8f8f
--- /dev/null
+++ b/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. \ No newline at end of file
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/__init__.py
diff --git a/admin.py b/admin.py
new file mode 100644
index 0000000..410866a
--- /dev/null
+++ b/admin.py
@@ -0,0 +1,6 @@
+from django.contrib import admin
+
+from .models import Thread, Comment
+
+admin.site.register(Thread)
+admin.site.register(Comment)
diff --git a/apps.py b/apps.py
new file mode 100644
index 0000000..ff01b77
--- /dev/null
+++ b/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class CommentsConfig(AppConfig):
+ name = 'comments'
diff --git a/ext.py b/ext.py
new file mode 100644
index 0000000..3e648f6
--- /dev/null
+++ b/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:])
diff --git a/models.py b/models.py
new file mode 100644
index 0000000..a82c7cb
--- /dev/null
+++ b/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
diff --git a/static/thread.css b/static/thread.css
new file mode 100644
index 0000000..bc23fd4
--- /dev/null
+++ b/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;
+} \ No newline at end of file
diff --git a/templates/comments/thread.html b/templates/comments/thread.html
new file mode 100644
index 0000000..d249925
--- /dev/null
+++ b/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>
diff --git a/tests.py b/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/urls.py b/urls.py
new file mode 100644
index 0000000..66f45c4
--- /dev/null
+++ b/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'),
+ ]
diff --git a/views.py b/views.py
new file mode 100644
index 0000000..0b689f0
--- /dev/null
+++ b/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,)))