poobrains/poobrains/commenting.py

357 lines
12 KiB
Python

# -*- coding: utf-8 -*-
import os
import random
import functools
import collections
import datetime
import peewee
import flask
import io
from PIL import Image, ImageDraw, ImageFont, ImageFilter
#import poobrains
from poobrains import app
import poobrains.helpers
import poobrains.rendering
import poobrains.storage
import poobrains.form
import poobrains.auth
import poobrains.tagging
class Comment(poobrains.auth.Administerable):
class Meta:
abstract = False
order_by = ['created']
model = poobrains.storage.fields.CharField()
handle = poobrains.storage.fields.CharField()
reply_to = poobrains.storage.fields.ForeignKeyField('self', null=True)
created = poobrains.storage.fields.DateTimeField(default=datetime.datetime.now, null=False)
author = poobrains.storage.fields.CharField()
text = poobrains.storage.fields.TextField()
def __setattr__(self, name, value):
if name == 'model':
valid_models = Commentable.class_children_keyed()
if not value in valid_models:
raise AttributeError(f'Invalid model "{value}" for Comment.')
super(Comment, self).__setattr__(name, value)
def thread(self):
#TODO: Move creating trees into a stored procedure for performance at some point
thread = collections.OrderedDict()
children = Comment.select().where(Comment.reply_to == self)
for child in children:
thread[child] = child.thread()
return thread
def child_comments(self):
return Comment.select().where(Comment.reply_to == self)
def reply_form(self):
children = Commentable.class_children_keyed()
if self.model in children:
model = Commentable.class_children_keyed()[self.model]
comments_enabled = model.load(self.handle).comments_enabled
if comments_enabled:
try:
self.permissions['create'].check(flask.g.user)
return CommentForm(self.model, self.handle, reply_to=self)
except poobrains.auth.AccessDenied:
return False
return poobrains.rendering.RenderString("Commenting is disabled.")
raise Exception("Bork")
@poobrains.auth.User.on_profile
class Commentable(poobrains.tagging.Taggable):
class Meta:
abstract = True
order_by = ['-date']
comments_enabled = poobrains.storage.fields.BooleanField(default=True, verbose_name=u'Enable comments')
notify_owner = poobrains.storage.fields.BooleanField(default=True, verbose_name='Notify owner', help_text='Whether to notify the owner of comments')
date = poobrains.storage.fields.DateTimeField(default=datetime.datetime.now, verbose_name='Date', help_text='Date of publication, current time if left empty')
@property
def date_pretty(self):
if isinstance(self.date, datetime.datetime):
return self.date.strftime('%a %b %d %Y - %H:%M:%S')
return 'Lost in Time'
@property
def comments(self):
return Comment.select().where(Comment.model == self.__class__.__name__, Comment.handle == self.handle_string)
@property
def comments_threaded(self):
comments_threaded = collections.OrderedDict()
try:
Comment.permissions['read'].check(flask.g.user)
for comment in self.comments.where(Comment.reply_to == None): # iterate through root comments
comments_threaded[comment] = comment.thread()
except poobrains.auth.AccessDenied:
pass # No point loading shit this user isn't allowed to render anyways.
return comments_threaded
def comment_form(self, reply_to=None):
if self.comments_enabled:
try:
Comment.permissions['create'].check(flask.g.user) # no form for users who aren't allowed to comment
return CommentForm(instance=self, reply_to=reply_to)
except poobrains.auth.AccessDenied:
return poobrains.rendering.RenderString("You are not allowed to post comments.")
return poobrains.rendering.RenderString("Commenting is disabled.")
class CommentForm(poobrains.form.Form):
instance = None # Commentable instance the comment is going to be associated to
reply_to = poobrains.form.fields.Value()
author = poobrains.form.fields.Text(required=True, placeholder='Yer name')
text = poobrains.form.fields.TextArea(required=True, placeholder='Yer mutterings')
submit = poobrains.form.Button('submit', label='Send comment')
def __init__(self, model=None, instance_handle=None, instance=None, **kwargs):
if not isinstance(instance, Commentable):
assert model and instance_handle, "Either instance (a Commentable instance) or model AND handle must be passed."
cls = Commentable.class_children_keyed()[model]
instance = cls.load(instance_handle)
reply_to = kwargs.pop('reply_to') if 'reply_to' in kwargs else None
if isinstance(reply_to, int):
reply_to = Comment.load(reply_to)
super(CommentForm, self).__init__(**kwargs)
self.instance = instance
self.fields['reply_to'].value = reply_to
self.action = "/comment/%s/%s" % (self.instance.__class__.__name__, self.instance.handle_string) # FIXME: This is shit. Maybe we want to teach Pooprint.get_view_url handling extra parameters from the URL?
if reply_to:
self.action += "/%d" % reply_to.id
def process(self, submit):
self.instance.permissions['read'].check(flask.g.user)
iteration_limit = 10
for i in range(0, iteration_limit):
name = poobrains.helpers.random_string_light(16).lower()
if not Challenge.select().where(Challenge.name == name).count():
break
elif i == iteration_limit - 1: # means loop ran through without finding a free challenge name
flask.flash(u"I'm sorry Dave. I'm afraid I can't do that.")
return flask.redirect(self.instance.url('full'))
challenge = Challenge()
challenge.name = name
challenge.model = self.instance.__class__.__name__
challenge.handle = self.instance.handle_string
challenge.reply_to = self.fields['reply_to'].value
challenge.author = self.fields['author'].value
challenge.text = self.fields['text'].value
challenge.save()
return flask.redirect(challenge.url('full'))
app.site.add_view(CommentForm, '/comment/<string:model>/<string:instance_handle>', mode='full')
app.site.add_view(CommentForm, '/comment/<string:model>/<string:instance_handle>/<int:reply_to>', mode='full')
class Challenge(poobrains.storage.Named):
class Meta:
modes = collections.OrderedDict([('full', 'read'), ('raw', 'read')])
title = 'Fuck bots, get bugs'
captcha = poobrains.storage.fields.CharField(default=functools.partial(poobrains.helpers.random_string_light, 6))
model = poobrains.storage.fields.CharField()
handle = poobrains.storage.fields.CharField()
reply_to = poobrains.storage.fields.ForeignKeyField(Comment, null=True)
created = poobrains.storage.fields.DateTimeField(default=datetime.datetime.now, null=False)
author = poobrains.storage.fields.CharField()
text = poobrains.storage.fields.TextField()
def view(self, mode=None, handle=None):
"""
view function to be called in a flask request context
"""
if mode == 'raw':
colors = [
(0,128,255),
(0,255,128),
(128,0,255),
(128,255,0),
(255,0,128),
(255,128,0)
]
font_path = os.path.join(app.poobrain_path, 'themes/default/fonts/knewave/knewave-outline.otf')
image = Image.new('RGBA', (250, 80), (255,255,255,0))
font = ImageFont.truetype(font_path, 42)
#x_jitter = ((image.width/10) * -1, 0)
#y_jitter = ((image.height/10) * -1, image.height/10)
x_jitter = (-5, 5)
y_jitter = (-5, 5)
textsize = font.getsize(' '.join(self.captcha))
centered = (image.width / 2 - textsize[0] / 2, image.height / 2 - textsize[1] / 2)
x = centered[0] + random.randint(x_jitter[0], x_jitter[1])
y = centered[1] + random.randint(y_jitter[0], y_jitter[1])
baseline = centered[1]
for char in self.captcha:
c = colors[random.randint(0, len(colors) -1)]
c = tuple(list(c) + [random.randint(255,255)])
char_size = font.getsize(char)
char_wrapped = ' %s ' % char
char_wrapped_size = font.getsize(char_wrapped)
char_layer = Image.new('RGBA', char_wrapped_size, (0,0,0,0))
char_draw = ImageDraw.Draw(char_layer)
char_draw.text((0,0), char_wrapped, c, font=font)
char_layer = char_layer.rotate(random.randint(-15, 15), expand=True, resample=Image.BICUBIC)
image.paste(
char_layer,
(int(x), int(y)),
mask=char_layer
)
x += char_size[0] + random.randint(x_jitter[0], x_jitter[1])
y = baseline + random.randint(y_jitter[0], y_jitter[1])
shine = image.filter(ImageFilter.GaussianBlur(radius=8))
image = Image.alpha_composite(image, shine)
out = io.BytesIO()
image.save(out, format='PNG')
return poobrains.Response(
out.getvalue(),
mimetype='image/png'
)
return ChallengeForm(self).view('full')
app.site.add_view(Challenge, '/comment/challenge/<handle>/', mode='full')
app.site.add_view(Challenge, '/comment/challenge/<handle>/raw', mode='raw')
class ChallengeForm(poobrains.form.Form):
challenge = None
response = poobrains.form.fields.Text()
submit = poobrains.form.Button('submit', label='Send')
def __init__(self, challenge):
super(ChallengeForm, self).__init__()
self.challenge = challenge
def validate(self, submit):
if not self.fields['response'].value == self.challenge.captcha:
self.challenge.captcha = self.challenge.__class__.captcha.default()
self.challenge.save()
raise poobrains.errors.ValidationError("A robot could do this better and cheaper than you.")
def process(self, submit):
try:
cls = Commentable.class_children_keyed()[self.challenge.model]
instance = cls.load(self.challenge.handle)
except KeyError:
flask.flash(u"WORNG!1!!", 'error')
app.logger.error("Challenge %s refers to non-existant model!" % self.challenge.name)
return flask.redirect('/')
except peewee.DoesNotExist:
flask.flash(u"The thing you wanted to comment on does not exist anymore.")
return flask.redirect(cls.url('teaser'))
comment = Comment()
comment.model = self.challenge.model
comment.handle = self.challenge.handle
comment.reply_to = self.challenge.reply_to
comment.author = self.challenge.author
comment.text = self.challenge.text
if comment.save():
flask.flash(u"Your comment has been saved.")
if instance.notify_owner:
instance.owner.notify("New comment on [%s/%s] by %s." % (self.challenge.model, self.challenge.handle, self.challenge.author))
self.challenge.delete_instance() # commit glorious seppuku
return flask.redirect(instance.url('full'))
flask.flash(u"Your comment could not be saved.", 'error')
@app.cron
def bury_orphaned_challenges():
deathwall = datetime.datetime.now() - datetime.timedelta(seconds=app.config['TOKEN_VALIDITY'])
q = Challenge.delete().where(Challenge.created <= deathwall)
count = q.execute()
app.logger.info("Deleted %d orphaned comment challenges." % count)