commit 8aee826f7ddc03fd20cadfe2a4fab5f521fdbc04 Author: Derek Stevens Date: Tue Apr 28 19:01:19 2020 -0400 first commit 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 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 + +# 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 + +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 %} + + + + + + + + comments for {{ thread.thread_id }} + + + + +
+ {% for comment in comments %} +
+
{{ comment.comment_author }}
+
{{ comment.comment_date }}
+
{{ comment.comment_data }}
+
+ {% empty %} +

No comments yet!

+ {% endfor %} + +

Post a comment

+ {% if error_message %} + {{ error_message }} + {% endif %} +
+
+ {% csrf_token %} + name:
+
+ email:
+
+ comment:
+
+ +

+
+
+ + 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('/', views.thread, name='thread'), + path('/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 + +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,)))