Compare commits

..

6 Commits

11 changed files with 499 additions and 18 deletions

View File

@ -40,7 +40,7 @@ class QuestConsumer(WebsocketConsumer):
return
self.events[event](self, data.get('data'))
def send(self, event, data):
def send(self, event, data={}):
"""
Overridden method. If a dictionary is provided, it is converted
to JSON before sending it.

View File

@ -6,10 +6,13 @@ QuestConsumer object in consumers.py.
import re
import time
import types
import random
import bleach
from django.utils.timezone import localtime
from quest.models import Message, Quest
from quest.models import Message, Quest, Post, DiceCall
from quest.forms import DiceCallForm
def message(socket, data):
"""
@ -19,12 +22,14 @@ def message(socket, data):
message = data.get('message')
quest_id = data.get('quest_id')
# cleaning
message = message.strip()
if not message:
return
tags = ["b", "code", "i", "s"]
message = bleach.clean(message, tags=tags)
# greentext
lines = []
for line in message.splitlines():
if line.startswith(">") and not line.startswith(">>"):
@ -32,6 +37,7 @@ def message(socket, data):
lines.append(line)
message = "<br>".join(lines)
# quote links
quotes = re.findall(r"&gt;&gt;\d+", message)
for quote in quotes:
msg_id = quote.replace("&gt;&gt;", "")
@ -41,6 +47,32 @@ def message(socket, data):
msg += 'onmouseout="clearPreview()">' + quote + '</a>'
message = message.replace(quote, msg)
# handle image
# dice rolling
if any(map(message.startswith, ["/dice", "/roll"])):
reg = re.search(r"(\d+)d(\d+)([+-]\d+)?", data["message"])
if not reg:
return
try:
groups = [0 if d is None else int(d) for d in reg.groups()]
dice_num, dice_sides, dice_mod = groups
assert 0 < dice_num < 100
assert 0 < dice_sides < 100
assert -1000 < dice_mod < 1000
except (ValueError, AssertionError):
return
dice = [random.randint(1, dice_sides) for _ in range(dice_num)]
total = sum(dice) + dice_mod
roll_msg = f"Rolled {', '.join(map(str, dice))}"
if dice_mod:
if dice_mod > 0:
roll_msg += " + " + str(dice_mod)
else:
roll_msg += " - " + str(dice_mod)[1:]
roll_msg += " = " + str(total)
message += '<hr class="msgSrvHr" /><b>' + roll_msg + "</b>"
user = socket.scope['user']
m = Message(
@ -58,6 +90,128 @@ def message(socket, data):
socket.send('message', data)
def text_post(socket, data):
"""
Called when the QM creates a new text post.
"""
# TODO: security
quest_id = data.get('quest_id')
post_text = data.get('text')
page_num = data.get('page_num')
# cleaning
post_text = bleach.clean(post_text.strip())
post_text = text.replace("\n", "<br>")
# handle image
p = Post(
quest=Quest.objects.get(id=quest_id),
page_num=page_num,
post_type='text',
post_text=post_text)
p.save()
data = {}
data['post_text'] = post_text
data['post_type'] = 'text'
data['date'] = localtime(p.timestamp).strftime('%Y-%m-%d %H:%M')
data['post_id'] = p.id
socket.send('new_post', data)
def dice_post(socket, data):
"""
Called when the QM makes a new dice post.
"""
quest_id = data.get('quest_id')
page_num = data.get('page_num')
form = DiceCallForm(data)
if not form.is_valid():
return # error message?
form = form.cleaned_data
dice_roll = str(form['diceNum']) + "d"
dice_roll += str(form['diceSides'])
if form['diceMod']:
if form['diceMod'] > 0:
dice_roll += "+"
dice_roll += str(form['diceMod'])
post_text = "Roll " + dice_roll
if form['diceChal']:
post_text += " vs DC" + str(form['diceChal'])
p = Post(
quest=Quest.objects.get(id=quest_id),
page_num=page_num,
post_type='dice',
post_text=post_text
)
p.save()
d = DiceCall(
post=p,
dice_roll=dice_roll,
strict=form['diceStrict'],
dice_challenge=form['diceChal'],
rolls_taken=form['diceRollsTaken'],
open=True,
)
d.save()
socket.send('close_all_posts')
data = {}
data['post_text'] = post_text
data['post_type'] = 'dice'
data['date'] = localtime(p.timestamp).strftime('%Y-%m-%d %H:%M')
data['post_id'] = p.id
socket.send('new_post', data)
def close_post(socket, data):
"""
Called when the QM closes an open post.
"""
post_id = data.get('post_id')
p = Post.objects.get(id=post_id)
if data.get('post_type') == 'dice':
p.dicecall.open = False
p.dicecall.save()
elif data.get('post_type') == 'poll':
p.poll.open = False
p.poll.save()
data = {}
data['post_id'] = post_id
socket.send('close_post', data)
def open_post(socket, data):
"""
Called when the QM opens a closed post.
"""
# TODO: only posts on last page can be opened
post_id = data.get('post_id')
p = Post.objects.get(id=post_id)
if data.get('post_type') == 'dice':
posts = Post.objects.filter(post_type='dice', dicecall__open=True)
for post in posts:
post.dicecall.open = False
post.dicecall.save()
socket.send('close_all_posts', {'post_type': 'dice'})
p.dicecall.open = True
p.dicecall.save()
elif data.get('post_type') == 'poll':
p.poll.open = True
p.poll.save()
data = {}
data['post_id'] = post_id
socket.send('open_post', data)
events = {}
for obj in dir():
if type(locals()[obj]) == types.FunctionType:

19
quest/forms.py Normal file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env python3
"""
Form(s) for the quest page.
"""
from django import forms
class DiceCallForm(forms.Form):
"""
The form for the QM making dice calls.
"""
diceNum = forms.IntegerField(min_value=1, max_value=99)
diceSides = forms.IntegerField(min_value=1, max_value=99)
diceMod = forms.IntegerField(
min_value=-999, max_value=999, required=False)
diceChal = forms.IntegerField(
min_value=1, max_value=999, required=False)
diceRollsTaken = forms.IntegerField(
min_value=1, max_value=99, required=False)
diceStrict = forms.BooleanField(required=False)

View File

@ -9,7 +9,7 @@
</script>
<script type="text/javascript" src="{{ static('quest.js') }}"></script>
{% if request.user == quest.owner %}
{#<script type="text/javascript" src="{{ static('questQM.js') }}"></script>#}
<script type="text/javascript" src="{{ static('questQM.js') }}"></script>
{% endif %}
<script>window.onload = load;</script>
{% endblock %}
@ -34,9 +34,9 @@
{% if post.post_type == "text" %}
<div class="questPost textPost">
{% elif post.post_type == "dice" %}
<div class="questPost dicePost{% if post == posts|last %} activePost{% endif %}">
<div class="questPost dicePost{% if post.dicecall.open %} activePost{% endif %}">
{% elif post.post_type == "poll" %}
<div class="questPost pollPost{% if post == posts|last %} activePost{% endif %}">
<div class="questPost pollPost{% if post.poll.open %} activePost{% endif %}">
{% endif %}
<div class="questPostMeta">
{{ localtime(post.timestamp).strftime('%Y-%m-%d %H:%M') }}
@ -44,9 +44,12 @@
{% if post.post_type == "text" %}
<br /><a href="javascript:void(0);" id="editPost-{{ post.id }}" onclick="edit_post({{ post.id }})">Edit</a>
<a href="javascript:void(0);" id="savePost-{{ post.id }}" onclick="save_post('{{ post.id }}')" style="display:none;">Save</a>
{% elif post.post_type in ("dice", "poll") and post == posts|last %}
<br /><a href="javascript:void(0);" id="closePost-{{ post.id }}" onclick="close_post({{ post.id }})"{% if post.id != quest.open_post_id %} style="display:none;"{% endif %}>Close</a>
<a href="javascript:void(0);" id="openPost-{{ post.id }}" onclick="open_post({{ post.id }})"{% if post.id == quest.open_post_id %} style="display:none;"{% endif %}>Open</a>
{% elif post.post_type == "dice" %}
<br /><a href="javascript:void(0);" id="closePost-{{ post.id }}" onclick="close_post_send({{ post.id }})"{% if not post.dicecall.open %} style="display:none;"{% endif %}>Close</a>
<a href="javascript:void(0);" id="openPost-{{ post.id }}" onclick="open_post_send({{ post.id }})"{% if post.dicecall.open %} style="display:none;"{% endif %}>Open</a>
{% elif post.post_type == "poll" %}
<br /><a href="javascript:void(0);" id="closePost-{{ post.id }}" onclick="close_post_send({{ post.id }})"{% if not post.poll.open %} style="display:none;"{% endif %}>Close</a>
<a href="javascript:void(0);" id="openPost-{{ post.id }}" onclick="open_post_send({{ post.id }})"{% if post.poll.open %} style="display:none;"{% endif %}>Open</a>
{% endif %}
{% endif %}
</div>
@ -56,13 +59,14 @@
{{ post.post_text }}
{% endautoescape %}
{% elif post.post_type == "dice" %}
<h3>{{ post.post_text }} - {% if post.id == quest.open_post_id %}Open{% else %}Closed{% endif %}</h3>
<h3>{{ post.post_text }} - {% if post.dicecall.open %}Open{% else %}Closed{% endif %}</h3>
{# for dice_roll in dice_rolls.get(post.id, []) %}
<div id="questRollId-{{ dice_roll[0] }}">
<b>Rolled {{ dice_roll[4] }} = {{ dice_roll[5] }} ({{ dice_roll[3] }}){% if post.id|dice_chal != 0 %} - {% if dice_roll[5] >= post.id|dice_chal %}Pass{% else %}Fail{% endif %}{% endif %}</b>
</div>
{% endfor #}
<h3>{{ post.post_test }} - {% if post.id == quest.open_post_id %}Open{% else %}Closed{% endif %}</h3>
{% elif post.post_type == "poll" %}
<h3>{{ post.post_text }} - {% if post.poll.open %}Open{% else %}Closed{% endif %}</h3>
<table class="poll" id="poll-{{ post.id }}">
{# for option in options.get(post.id, []) %}
<tr id="optionRow-{{ option[0] }}">
@ -109,7 +113,7 @@
<span class="tooltip" title="Only take matching rolls.">Strict</span><br />
<input type="checkbox" onclick="document.getElementById('diceChal').disabled=!this.checked;"/>
<span class="tooltip" title="Dice challenge">DC:</span>
<input type="number" name="diceChal" id="diceChal" min="1" max="99" disabled/><br />
<input type="number" name="diceChal" id="diceChal" min="1" max="999" disabled/><br />
<input type="checkbox" onclick="document.getElementById('diceRollsTaken').disabled=!this.checked;"/>
<span class="tooltip" title="Automatically close the dice call after this many rolls have been made.">Rolls Taken:</span>
<input type="number" name="diceRollsTaken" id="diceRollsTaken" min="1" max="99" disabled/><br />

View File

@ -0,0 +1,25 @@
# Generated by Django 2.1 on 2018-08-23 15:46
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('quest', '0006_auto_20180817_0921'),
]
operations = [
migrations.CreateModel(
name='DiceCall',
fields=[
('post', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='quest.Post')),
('dice_roll', models.CharField(max_length=9)),
('strict', models.BooleanField()),
('dice_challenge', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(999), django.core.validators.MinValueValidator(1)])),
('rolls_taken', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(99), django.core.validators.MinValueValidator(1)])),
],
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.1 on 2018-08-24 12:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('quest', '0007_dicecall'),
]
operations = [
migrations.RemoveField(
model_name='quest',
name='open_post_id',
),
migrations.AddField(
model_name='dicecall',
name='open',
field=models.BooleanField(default=False),
),
]

View File

@ -4,6 +4,7 @@ The main quest models.
"""
from django.db import models
from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator
class Quest(models.Model):
"""
@ -13,7 +14,6 @@ class Quest(models.Model):
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
open_post_id = models.IntegerField(null=True)
class Post(models.Model):
@ -25,11 +25,43 @@ class Post(models.Model):
POST_TYPES = (
('text', 'Text'),
('dice', 'Dice'),
('poll', 'Poll'))
('poll', 'Poll')
)
post_type = models.CharField(max_length=4, choices=POST_TYPES)
post_text = models.TextField()
timestamp = models.DateTimeField(auto_now=True)
class DiceCall(models.Model):
"""
An object representing dice calls made by the QM.
"""
post = models.OneToOneField(
Post,
on_delete=models.CASCADE,
primary_key=True,
)
dice_roll = models.CharField(max_length=9)
strict = models.BooleanField()
dice_challenge = models.IntegerField(
null=True,
blank=True,
validators=[
MaxValueValidator(999),
MinValueValidator(1)
]
)
rolls_taken = models.IntegerField(
null=True,
blank=True,
validators=[
MaxValueValidator(99),
MinValueValidator(1)
]
)
open = models.BooleanField(default=False)
class PageTitle(models.Model):
"""
Represents the title of a quest page.
@ -41,6 +73,7 @@ class PageTitle(models.Model):
def __str__(self):
return self.title
class Message(models.Model):
"""
Represents a chat message.

View File

@ -15,8 +15,7 @@ h3 {
#questPane {
padding-left: 5%;
padding-right: 35%;
width: 100%;
width: 65%;
}
.questPost {

View File

@ -27,7 +27,7 @@ function load() {
});
}
/* Websocket events */
/* Websocket receive */
socket.events['message'] = function(data) {
let msg_str = '<div id="message-' + data.message_id + '" class="message">';
msg_str = '<div class="messageHeader"><span class="messageName">' + data.name + '</span> ';
@ -41,6 +41,60 @@ socket.events['message'] = function(data) {
mbox.scrollTop = mbox.scrollHeight;
}
}
socket.events['new_post'] = function(data) {
//deactivate_post();
let qposts = document.getElementById('questPosts');
let post_str = '<div class="questPost ';
if (data.post_type == 'text') {
post_str += 'textPost">';
} else if (data.post_type == 'dice') {
post_str += 'dicePost active_post">';
} else if (data.post_type == 'poll') {
post_str += 'pollPost active_post">';
}
post_str += '<div class="questPostMeta">' + data.date;
post_str += '</div><div class="questPostData" id="questPostData-' + data.post_id + '">';
if (data.post_type == 'text') {
post_str += data.post_text;
} else if (data.post_type == 'dice') {
post_str += '<h3>' + data.post_text + ' - Open</h3>';
} else if (data.post_type == 'poll') {
post_str += '<h3>' + data.post_text + ' - Open</h3>';
post_str += '<table class="poll" id="poll-' + data.post_id + '">';
post_str += '<col/><col/><col/>';
for (i = 0; i < data.options.length; i++) {
post_str += '<tr id="optionRow-' + data.options[i][0] + '">';
post_str += '<td class="pollCheckBox"><input type="checkbox" id="pollInput-' + data.options[i][0] + '" onchange="pollVote(' + data.post_id + ',' + data.options[i][0] + ')"/>';
post_str += '<label for="pollInput-' + data.options[i][0] + '"></label></td>';
post_str += '<td class="option_text">' + data.options[i][1] + '</td>';
post_str += '<td class="optionVotes">0</td></tr>';
}
post_str += '</table>';
if (data.allow_writein) {
post_str += '<div id="writeinContainer">';
post_str += 'Write-in: <input type="text" id="writeinInput" placeholder="Custom choice..." maxlength="200" /><br />';
post_str += '<input type="submit" id="writeinSubmit" value="Submit" onclick="submitWritein({{ quest_post[0] }});"/></div>';
}
}
post_str += '</div></div><br />';
qposts.innerHTML = qposts.innerHTML + post_str;
};
socket.events['close_post'] = function(data) {
close_post(data.post_id);
}
socket.events['open_post'] = function(data) {
open_post(data.post_id);
}
socket.events['close_all_posts'] = function(data) {
let class_set = '';
if (data.post_type == 'dice') { class_set = 'dicePost activePost'; }
else if (data.post_type == 'poll') { class_set = 'pollPost activePost'; }
else { class_set = 'activePost'; }
let posts = document.getElementsByClassName(class_set);
for (let i = 0; i < posts.length; i++) {
close_post(posts[i].children[1].id.slice(14)); // retreive the id number at the end
}
}
/* Helpers */
function padToTwo(number) {
@ -54,7 +108,7 @@ function strftime(date) {
return date_str;
}
/* User-facing */
/* DOM editing */
function quote(message_id) {
let textbox = document.getElementById('messageTextArea');
textbox.value += '>>' + message_id + '\n';
@ -82,3 +136,35 @@ function scrollToMsg(message_id) {
if (!elem) { return; }
elem.scrollIntoView();
}
function close_post(post_id) {
let post = document.getElementById('questPostData-' + post_id);
post.children[0].textContent = post.children[0].textContent.replace('Open', 'Closed');
if (post.parentElement.classList.contains('pollPost')) {
let table = document.getElementById('poll-' + post_id);
let boxes = table.getElementsByTagName('input');
for (let i = 0; i < boxes.length; i++) {
boxes[i].disabled = true;
}
let writein = document.getElementById('writeinContainer');
if (writein) {
writein.style.display = 'none';
}
}
}
function open_post(post_id) {
let post = document.getElementById('questPostData-' + post_id);
post.firstElementChild.textContent = post.firstElementChild.textContent.replace('Closed', 'Open');
post.parentElement.className += ' activePost';
console.log(post.parentElement.className);
if (post.parentElement.classList.contains('pollPost')) {
let table = document.getElementById('poll-' + post_id);
let boxes = table.getElementsByTagName('input');
for (let i = 0; i < boxes.length; i++) {
boxes[i].disabled = false;
}
let writein = document.getElementById('writeinContainer');
if (writein) {
writein.style.display = 'initial';
}
}
}

139
quest/static/questQM.js Normal file
View File

@ -0,0 +1,139 @@
/* Websocket receive */
socket.events['new_post'] = function(data) {
//deactivate_post();
let qposts = document.getElementById('questPosts');
let post_str = '<div class="questPost ';
if (data.post_type == 'text') {
post_str += 'textPost">';
} else if (data.post_type == 'dice') {
post_str += 'dicePost active_post">';
} else if (data.post_type == 'poll') {
post_str += 'pollPost active_post">';
}
post_str += '<div class="questPostMeta">' + data.date;
/* QM only */
if (data.post_type == 'text') {
post_str += '<br /><a href="javascript:void(0);" onclick="edit_post(\'' + data.post_id + '\')">Edit</a>';
post_str += '<a href="javascript:void(0);" id="savePost-' + data.post_id + '" onclick="save_post(\'' + data.post_id + '\')" style="display:none;">Save</a>';
} else if (data.post_type == 'dice' || data.post_type == 'poll') {
post_str += '<br /><a href="javascript:void(0);" id="close_post_id-' + data.post_id + '" onclick="close_post(' + data.post_id + ')">Close</a>';
post_str += '<a href="javascript:void(0);" id="open_post_id-' + data.post_id + '" onclick="open_post(' + data.post_id + ')" style="display:none;">Open</a>'
}
/* end QM only */
post_str += '</div><div class="questPostData" id="questPostData-' + data.post_id + '">';
if (data.post_type == 'text') {
post_str += data.post_text;
} else if (data.post_type == 'dice') {
post_str += '<h3>' + data.post_text + ' - Open</h3>';
} else if (data.post_type == 'poll') {
post_str += '<h3>' + data.post_text + ' - Open</h3>';
post_str += '<table class="poll" id="poll-' + data.post_id + '">';
post_str += '<col/><col/><col/>';
for (i = 0; i < data.options.length; i++) {
post_str += '<tr id="optionRow-' + data.options[i][0] + '">';
post_str += '<td class="pollCheckBox"><input type="checkbox" id="pollInput-' + data.options[i][0] + '" onchange="pollVote(' + data.post_id + ',' + data.options[i][0] + ')"/>';
post_str += '<label for="pollInput-' + data.options[i][0] + '"></label></td>';
post_str += '<td class="option_text">' + data.options[i][1] + '</td>';
post_str += '<td class="optionVotes">0</td></tr>';
}
post_str += '</table>';
if (data.allow_writein) {
post_str += '<div id="writeinContainer">';
post_str += 'Write-in: <input type="text" id="writeinInput" placeholder="Custom choice..." maxlength="200" /><br />';
post_str += '<input type="submit" id="writeinSubmit" value="Submit" onclick="submitWritein({{ quest_post[0] }});"/></div>';
}
}
post_str += '</div></div><br />';
qposts.innerHTML = qposts.innerHTML + post_str;
};
/* Websocket send */
function makePost() {
let qparea = document.getElementById('postTextArea');
let text = qparea.value.trim();
qparea.value = '';
if (text == '') { return; }
socket.send('text_post', {text: text, page_num: page_num, quest_id: quest_id});
}
function form_post(form_id, event) {
let formData = new FormData(document.getElementById(form_id));
let obj = {};
formData.forEach(function(value, key) {
obj[key] = value;
});
obj.quest_id = quest_id;
obj.page_num = page_num;
socket.send(event, obj);
document.getElementById(form_id).reset();
}
function close_post_send(post_id) {
let post = document.getElementById('questPostData-' + post_id);
if (post.parentElement.classList.contains('dicePost')) {
data = {post_type: 'dice', post_id: post_id};
} else if (post.parentElement.classList.contains('pollPost')) {
data = {post_type: 'poll', post_id: post_id};
}
socket.send('close_post', data);
}
function open_post_send(post_id) {
let post = document.getElementById('questPostData-' + post_id);
if (post.parentElement.classList.contains('dicePost')) {
data = {post_type: 'dice', post_id: post_id};
} else if (post.parentElement.classList.contains('pollPost')) {
data = {post_type: 'poll', post_id: post_id};
}
socket.send('open_post', data);
}
/* DOM editing */
function openPostTab(event, modeName) {
let QMPostTabContent = document.getElementsByClassName("QMPostTabContent");
for (let i = 0; i < QMPostTabContent.length; i++) {
QMPostTabContent[i].style.display = "none";
}
let QMPostTab = document.getElementsByClassName("QMPostTab");
for (let i = 0; i < QMPostTab.length; i++) {
QMPostTab[i].className = QMPostTab[i].className.replace(" active", "");
}
document.getElementById(modeName).style.display = "block";
event.currentTarget.className += " active";
}
function close_post(post_id) {
let post = document.getElementById('questPostData-' + post_id);
post.children[0].textContent = post.children[0].textContent.replace('Open', 'Closed');
/* QM only */
document.getElementById('closePost-' + post_id).style.display = 'none';
document.getElementById('openPost-' + post_id).style.display = 'initial';
/* end QM only */
if (post.parentElement.classList.contains('pollPost')) {
let table = document.getElementById('poll-' + post_id);
let boxes = table.getElementsByTagName('input');
for (let i = 0; i < boxes.length; i++) {
boxes[i].disabled = true;
}
let writein = document.getElementById('writeinContainer');
if (writein) {
writein.style.display = 'none';
}
}
}
function open_post(post_id) {
let post = document.getElementById('questPostData-' + post_id);
post.firstElementChild.textContent = post.firstElementChild.textContent.replace('Closed', 'Open');
post.parentElement.className += ' activePost';
/* QM only */
document.getElementById('closePost-' + post_id).style.display = 'initial';
document.getElementById('openPost-' + post_id).style.display = 'none';
/* end QM only */
if (post.parentElement.classList.contains('pollPost')) {
let table = document.getElementById('poll-' + post_id);
let boxes = table.getElementsByTagName('input');
for (let i = 0; i < boxes.length; i++) {
boxes[i].disabled = false;
}
let writein = document.getElementById('writeinContainer');
if (writein) {
writein.style.display = 'initial';
}
}
}

View File

@ -119,7 +119,7 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'America/Chicago'
TIME_ZONE = 'America/New_York'
USE_I18N = False