Compare commits

...

4 Commits

Author SHA1 Message Date
a485a36897 poll voting works 2018-09-03 17:59:41 -04:00
738591b05e expose visitor's real ip to websocket 2018-08-30 23:33:25 -04:00
b5f8f69f69 get the visitor's real ip address 2018-08-30 13:33:08 -04:00
1a21c882a2 added poll posting 2018-08-29 14:01:31 -04:00
15 changed files with 367 additions and 32 deletions

View File

@ -65,6 +65,14 @@ class QuestConsumer(WebsocketConsumer):
'message': data 'message': data
} }
) )
def self_send(self, event, data={}):
"""
Like `send`, except only sends events to the paired websocket
instead of the entire group.
"""
data = json.dumps({'event': event, 'data': data})
self.dispatch_send({'message': data})
def dispatch_send(self, event): def dispatch_send(self, event):
""" """

View File

@ -4,6 +4,7 @@ Individual functions for handling WebSocket events. Gets called by the
QuestConsumer object in consumers.py. QuestConsumer object in consumers.py.
""" """
# TODO: quest owner only events # TODO: quest owner only events
# TODO: try/except on database calls
import re import re
import time import time
import types import types
@ -12,8 +13,8 @@ import random
import bleach import bleach
from django.utils.timezone import localtime from django.utils.timezone import localtime
from quest.models import Message, Quest, Post, DiceCall, DiceRoll from quest.models import *
from quest.forms import DiceCallForm from quest.forms import DiceCallForm, PollForm
def message(socket, data): def message(socket, data):
""" """
@ -43,9 +44,9 @@ def message(socket, data):
for quote in quotes: for quote in quotes:
msg_id = quote.replace(">>", "") msg_id = quote.replace(">>", "")
msg = '<a class="quotelink" ' msg = '<a class="quotelink" '
msg += 'href="javascript:scrollToMsg(\'' + msg_id + '\')" ' msg += 'href="javascript:scroll_to_msg(\'' + msg_id + '\')" '
msg += 'onmousemove="showPreview(event, \'' + msg_id + '\')" ' msg += 'onmousemove="show_preview(event, \'' + msg_id + '\')" '
msg += 'onmouseout="clearPreview()">' + quote + '</a>' msg += 'onmouseout="clear_preview()">' + quote + '</a>'
message = message.replace(quote, msg) message = message.replace(quote, msg)
# handle image # handle image
@ -209,10 +210,53 @@ def dice_post(socket, data):
dc.save() dc.save()
data = {} 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 data['post_id'] = p.id
data['post_type'] = 'dice'
data['post_text'] = post_text
data['date'] = localtime(p.timestamp).strftime('%Y-%m-%d %H:%M')
socket.send('new_post', data)
def poll_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 = PollForm(data)
if not form.is_valid():
return # error message?
form = form.cleaned_data
p = Post(
quest=Quest.objects.get(id=quest_id),
page_num=page_num,
post_type='poll',
post_text="Poll"
)
p.save()
pl = Poll(
post=p,
multi_choice=form.pop('multi_choice'),
allow_writein=form.pop('allow_writein'),
open=True
)
pl.save()
options = []
for key, option in form.items():
o = PollOption(
poll=pl,
text=option
)
o.save()
options.append(o)
data = {}
data['post_type'] = 'poll'
data['post_id'] = p.id
data['post_text'] = "Poll"
data['date'] = localtime(p.timestamp).strftime('%Y-%m-%d %H:%M')
data['options'] = [(o.id, o.text) for o in options]
socket.send('new_post', data) socket.send('new_post', data)
@ -262,6 +306,49 @@ def open_post(socket, data):
socket.send('open_post', data) socket.send('open_post', data)
def vote(socket, data):
"""
Called when a player votes in a poll.
"""
quest_id = data.get('quest_id')
post_id = data.get('post_id')
option_id = data.get('option_id')
polarity = data.get('polarity')
ip_address = socket.scope['client'][0]
user = socket.scope['user']
if polarity == False:
v = PollVote.objects.get(ip_address=ip_address, option__id=option_id)
v.delete()
else:
p = Poll.objects.get(post_id=post_id)
if p.multi_choice == False:
votes = PollVote.objects.filter(
ip_address=ip_address,
option__poll=p
)
for vote in votes:
vote.delete()
data = {}
data['option_id'] = vote.option.id
data['polarity'] = False
socket.send('vote', data)
socket.self_send('set_option_box', data)
v = PollVote(
option=PollOption.objects.get(id=option_id),
ip_address=ip_address
)
if user.username:
v.user = user
v.save()
data = {}
data['option_id'] = option_id
data['polarity'] = polarity
socket.send('vote', data)
events = {} events = {}
for obj in dir(): for obj in dir():
if type(locals()[obj]) == types.FunctionType: if type(locals()[obj]) == types.FunctionType:

View File

@ -17,3 +17,22 @@ class DiceCallForm(forms.Form):
diceRollsTaken = forms.IntegerField( diceRollsTaken = forms.IntegerField(
min_value=1, max_value=99, required=False) min_value=1, max_value=99, required=False)
diceStrict = forms.BooleanField(required=False) diceStrict = forms.BooleanField(required=False)
class PollForm(forms.Form):
"""
The form for the QM making new polls.
"""
multi_choice = forms.BooleanField(required=False)
allow_writein = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
super(PollForm, self).__init__(*args, **kwargs)
data = args[0] # it's not nice to assume
options = {k: v for k, v in data.items() if k.startswith('pollOption')}
if len(options) > 20:
return
for key, value in options.items():
if not value:
continue
self.fields[key] = forms.CharField(max_length=200)

View File

@ -68,16 +68,16 @@
{% elif post.post_type == "poll" %} {% elif post.post_type == "poll" %}
<h3>{{ post.post_text }} - {% if post.poll.open %}Open{% else %}Closed{% endif %}</h3> <h3>{{ post.post_text }} - {% if post.poll.open %}Open{% else %}Closed{% endif %}</h3>
<table class="poll" id="poll-{{ post.id }}"> <table class="poll" id="poll-{{ post.id }}">
{# for option in options.get(post.id, []) %} {% for option in poll_options.filter(poll=post.poll).order_by("id") %}
<tr id="optionRow-{{ option[0] }}"> <tr id="optionRow-{{ option.id }}">
<td class="pollCheckBox"> <td class="pollCheckBox">
<input type="checkbox" {% if ip_address in poll_votes.get(option[0], []) %}checked="true"{% endif %} id="pollInput-{{ option[0] }}" onchange="pollVote({{ post.id }}, {{ option[0] }})"{% if post.id != quest.open_post_id %} disabled{% endif %}/> <input type="checkbox" {# if ip_address in poll_votes.get(option.id) %}checked="true"{% endif #} id="pollInput-{{ option.id }}" onchange="vote({{ post.id }}, {{ option.id }})"{% if not post.poll.open %} disabled{% endif %}/>
<label for="pollInput-{{ option[0] }}"></label> <label for="pollInput-{{ option.id }}"></label>
</td> </td>
<td class="option_text">{{ option[2] }}</td> <td class="option_text">{{ option.text }}</td>
<td class="optionVotes">{{ poll_votes.get(option[0], [])|length }}</td> <td class="optionVotes">{{ poll_votes.filter(option=option).count() }}</td>
</tr> </tr>
{% endfor #} {% endfor %}
</table> </table>
{# if post.id == quest.open_post_id and post.id|is_write_in %} {# if post.id == quest.open_post_id and post.id|is_write_in %}
<div id="writeinContainer"> <div id="writeinContainer">
@ -130,8 +130,8 @@
<div><input type="text" name="pollOption-2" class="pollOption" placeholder="Option 2" maxlength="200" /></div> <div><input type="text" name="pollOption-2" class="pollOption" placeholder="Option 2" maxlength="200" /></div>
</div> </div>
<hr> <hr>
<input type="checkbox" name="pollAllowMultipleChoices" />Allow multiple choices<br /> <input type="checkbox" name="multi_choice" />Allow multiple choices<br />
<input type="checkbox" name="pollAllowUserOptions" />Allow user-created options<br /> <input type="checkbox" name="allow_writein" />Allow user-created options<br />
<input type="submit" name="submit" value="Submit" /> <input type="submit" name="submit" value="Submit" />
</form> </form>
</div> </div>

View File

@ -0,0 +1,47 @@
# Generated by Django 2.1 on 2018-08-29 17:08
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('quest', '0009_diceroll'),
]
operations = [
migrations.CreateModel(
name='Poll',
fields=[
('post', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='quest.Post')),
('multi_choice', models.BooleanField()),
('allow_writein', models.BooleanField()),
('open', models.BooleanField()),
],
),
migrations.CreateModel(
name='PollOption',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.CharField(max_length=200)),
('poll', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='quest.Poll')),
],
),
migrations.CreateModel(
name='PollVote',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ip_address', models.GenericIPAddressField()),
('option', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='quest.PollOption')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AlterField(
model_name='dicecall',
name='open',
field=models.BooleanField(),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.1 on 2018-09-03 00:07
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('quest', '0010_auto_20180829_1308'),
]
operations = [
migrations.AlterField(
model_name='pollvote',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -73,7 +73,7 @@ class DiceCall(models.Model):
MinValueValidator(1) MinValueValidator(1)
] ]
) )
open = models.BooleanField(default=False) open = models.BooleanField()
class DiceRoll(models.Model): class DiceRoll(models.Model):
@ -91,6 +91,42 @@ class DiceRoll(models.Model):
total = models.IntegerField() total = models.IntegerField()
class Poll(models.Model):
"""
An object representing polls made by the QM.
"""
post = models.OneToOneField(
Post,
on_delete=models.CASCADE,
primary_key=True,
)
multi_choice = models.BooleanField()
allow_writein = models.BooleanField()
open = models.BooleanField()
class PollOption(models.Model):
"""
Represents options (choices) attached to a given poll. These can be
added by the QM upon creation, or by users through write-ins.
"""
poll = models.ForeignKey(Poll, on_delete=models.CASCADE)
text = models.CharField(max_length=200)
class PollVote(models.Model):
"""
Represents a user voting for an option.
"""
option = models.ForeignKey(PollOption, on_delete=models.CASCADE)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
blank=True,
null=True)
ip_address = models.GenericIPAddressField()
class PageTitle(models.Model): class PageTitle(models.Model):
""" """
Represents the title of a quest page. Represents the title of a quest page.

View File

@ -42,7 +42,6 @@ socket.events['message'] = function(data) {
} }
} }
socket.events['new_post'] = function(data) { socket.events['new_post'] = function(data) {
//deactivate_post();
let qposts = document.getElementById('questPosts'); let qposts = document.getElementById('questPosts');
let post_str = '<div class="questPost '; let post_str = '<div class="questPost ';
if (data.post_type === 'text') { if (data.post_type === 'text') {
@ -61,10 +60,9 @@ socket.events['new_post'] = function(data) {
} else if (data.post_type === 'poll') { } else if (data.post_type === 'poll') {
post_str += '<h3>' + data.post_text + ' - Open</h3>'; post_str += '<h3>' + data.post_text + ' - Open</h3>';
post_str += '<table class="poll" id="poll-' + data.post_id + '">'; post_str += '<table class="poll" id="poll-' + data.post_id + '">';
post_str += '<col/><col/><col/>'; for (let i = 0; i < data.options.length; i++) {
for (i = 0; i < data.options.length; i++) { post_str += '<tr id="optionRow-' + data.options[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="vote(' + data.post_id + ',' + 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 += '<label for="pollInput-' + data.options[i][0] + '"></label></td>';
post_str += '<td class="option_text">' + data.options[i][1] + '</td>'; post_str += '<td class="option_text">' + data.options[i][1] + '</td>';
post_str += '<td class="optionVotes">0</td></tr>'; post_str += '<td class="optionVotes">0</td></tr>';
@ -105,7 +103,7 @@ socket.events['update_post'] = function(data) {
let row = post.children[1].insertRow(-1); let row = post.children[1].insertRow(-1);
let cell = row.insertCell(0); let cell = row.insertCell(0);
cell.className = 'pollCheckBox'; cell.className = 'pollCheckBox';
cell.innerHTML = '<input type="checkbox" id="pollInput-' + data.options_id + '" onchange="pollVote(' + data.post_id + ',' + data.options_id + ')"/>'; cell.innerHTML = '<input type="checkbox" id="pollInput-' + data.options_id + '" onchange="vote(' + data.post_id + ',' + data.options_id + ')"/>';
cell.innerHTML += '<label for="pollInput-' + data.options_id + '"></label>'; cell.innerHTML += '<label for="pollInput-' + data.options_id + '"></label>';
cell = row.insertCell(1); cell = row.insertCell(1);
@ -117,6 +115,31 @@ socket.events['update_post'] = function(data) {
cell.innerHTML = "0"; cell.innerHTML = "0";
} }
} }
socket.events['vote'] = function(data) {
let row = document.getElementById('optionRow-' + data.option_id);
if (data.polarity) {
row.cells[2].textContent = Number(row.cells[2].textContent) + 1;
} else {
row.cells[2].textContent = Number(row.cells[2].textContent) - 1;
}
let table = row.parentElement.parentElement;
arr = Array.prototype.slice.call(table.rows);
arr.sort(sort_by_votes);
let new_tbody = document.createElement('tbody');
for (let i = 0; i < arr.length; i++) {
new_tbody.appendChild(arr[i]);
}
table.replaceChild(new_tbody, table.children[0]);
}
socket.events['set_option_box'] = function(data) {
document.getElementById('pollInput-' + data.option_id).checked = data.polarity;
}
/* Websocket send */
function vote(post_id, option_id) {
let polarity = document.getElementById('pollInput-' + option_id).checked;
socket.send('vote', {post_id: post_id, option_id: option_id, polarity: polarity, quest_id: quest_id});
}
/* Helpers */ /* Helpers */
function padToTwo(number) { function padToTwo(number) {
@ -129,6 +152,9 @@ function strftime(date) {
date_str += padToTwo(date.getHours()) + ':' + padToTwo(date.getMinutes()) + ':' + padToTwo(date.getSeconds()); date_str += padToTwo(date.getHours()) + ':' + padToTwo(date.getMinutes()) + ':' + padToTwo(date.getSeconds());
return date_str; return date_str;
} }
function sort_by_votes(a, b) {
return b.cells[2].textContent.localeCompare(a.cells[2].textContent);
}
/* DOM editing */ /* DOM editing */
function quote(message_id) { function quote(message_id) {
@ -136,7 +162,7 @@ function quote(message_id) {
textbox.value += '>>' + message_id + '\n'; textbox.value += '>>' + message_id + '\n';
textbox.focus(); textbox.focus();
} }
function showPreview(event, message_id) { function show_preview(event, message_id) {
let elem = document.getElementById('msg-' + message_id); let elem = document.getElementById('msg-' + message_id);
if (!elem) { return; } if (!elem) { return; }
let preview = document.getElementById('preview'); let preview = document.getElementById('preview');
@ -149,11 +175,11 @@ function showPreview(event, message_id) {
preview.style.left = x; preview.style.left = x;
preview.style.maxWidth = maxWidth; preview.style.maxWidth = maxWidth;
} }
function clearPreview() { function clear_preview() {
document.getElementById('preview').innerHTML = ''; document.getElementById('preview').innerHTML = '';
document.getElementById('preview').style.display = 'none'; document.getElementById('preview').style.display = 'none';
} }
function scrollToMsg(message_id) { function scroll_to_msg(message_id) {
let elem = document.getElementById('msg-' + message_id); let elem = document.getElementById('msg-' + message_id);
if (!elem) { return; } if (!elem) { return; }
elem.scrollIntoView(); elem.scrollIntoView();

View File

@ -1,6 +1,5 @@
/* Websocket receive */ /* Websocket receive */
socket.events['new_post'] = function(data) { socket.events['new_post'] = function(data) {
//deactivate_post();
let qposts = document.getElementById('questPosts'); let qposts = document.getElementById('questPosts');
let post_str = '<div class="questPost '; let post_str = '<div class="questPost ';
if (data.post_type === 'text') { if (data.post_type === 'text') {
@ -28,10 +27,9 @@ socket.events['new_post'] = function(data) {
} else if (data.post_type === 'poll') { } else if (data.post_type === 'poll') {
post_str += '<h3>' + data.post_text + ' - Open</h3>'; post_str += '<h3>' + data.post_text + ' - Open</h3>';
post_str += '<table class="poll" id="poll-' + data.post_id + '">'; post_str += '<table class="poll" id="poll-' + data.post_id + '">';
post_str += '<col/><col/><col/>';
for (i = 0; i < data.options.length; i++) { for (i = 0; i < data.options.length; i++) {
post_str += '<tr id="optionRow-' + data.options[i][0] + '">'; 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 += '<td class="pollCheckBox"><input type="checkbox" id="pollInput-' + data.options[i][0] + '" onchange="vote(' + data.post_id + ',' + data.options[i][0] + ')"/>';
post_str += '<label for="pollInput-' + data.options[i][0] + '"></label></td>'; 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="option_text">' + data.options[i][1] + '</td>';
post_str += '<td class="optionVotes">0</td></tr>'; post_str += '<td class="optionVotes">0</td></tr>';
@ -139,3 +137,15 @@ function open_post(post_id) {
} }
} }
} }
function insertPollOption() {
let opts = document.getElementById('pollOptions');
let num = opts.children.length+1;
let temp = document.createElement('template');
temp.innerHTML = '<div><input type="text" name="pollOption-' + num + '" class="pollOption" placeholder="Option ' + num + '" maxlength="200" /></div>';
opts.appendChild(temp.content);
}
function removePollOption() {
let opts = document.getElementById('pollOptions');
if (opts.children.length == 0) { return; }
opts.children[opts.children.length-1].outerHTML = '';
}

View File

@ -5,7 +5,7 @@ Quest and quest accessory views.
from django.shortcuts import render from django.shortcuts import render
from django.http import HttpResponse from django.http import HttpResponse
from .models import Quest, DiceRoll from .models import Quest, DiceRoll, PollOption, PollVote
def index(request): def index(request):
""" """
@ -23,5 +23,8 @@ def quest(request, quest_id, page_num=1):
messages = quest.message_set.all() messages = quest.message_set.all()
posts = quest.post_set.all() posts = quest.post_set.all()
dice_rolls = DiceRoll.objects.filter(dicecall__post__quest__id=quest_id) dice_rolls = DiceRoll.objects.filter(dicecall__post__quest__id=quest_id)
poll_options = PollOption.objects.filter(poll__post__quest__id=quest_id)
poll_votes = PollVote.objects.filter(option__poll__post__quest__id=quest_id)
ip_address = request.META['REMOTE_ADDR']
context = locals() context = locals()
return render(request, 'quest/quest.html', context) return render(request, 'quest/quest.html', context)

35
real_ip.conf Normal file
View File

@ -0,0 +1,35 @@
# This is an example nginx configuration file for setting the real IP from
# CloudFlare for the app to use. Nginx must be built with the
# `http_realip_module` module enabled. The virtual host/server block
# serving the site should contain the line
# `proxy_set_header X-Real-IP $http_cf_connecting_ip;`.
# The list of IP prefixes should be updated periodically from
# `https://www.cloudflare.com/ips/`. See `titivillus/middleware.py` for how
# to use the new header in application.
# Setting the real IP from CloudFlare
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 104.16.0.0/12;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2c0f:f248::/32;
set_real_ip_from 2a06:98c0::/29;
# use any of the following two
real_ip_header CF-Connecting-IP;
#real_ip_header X-Forwarded-For;

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
"""
Some custom asgi middleware for titivillus.
"""
from channels.auth import AuthMiddlewareStack
class XRealIPMiddleware(object):
"""
Attempts to set the get the visitor's real IP by grabbing the
`X-Real-IP` header from the request.
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
headers = dict(scope['headers'])
if b'x-real-ip' in headers:
scope['client'][0] = headers[b'x-real-ip'].decode('utf-8')
return self.inner(scope)

20
titivillus/middleware.py Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
"""
Some custom middleware for titivillus.
"""
class XRealIPMiddleware(object):
"""
Attempts to set the get the visitor's real IP by grabbing the
`X-Real-IP` header from the request.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
try:
real_ip = request.META['HTTP_X_REAL_IP']
except KeyError:
pass
else:
request.META['REMOTE_ADDR'] = real_ip
return self.get_response(request)

View File

@ -6,9 +6,14 @@ from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
import quest.routing import quest.routing
from .asgi_middleware import XRealIPMiddleware
CustomMiddlewareStack = lambda inner: XRealIPMiddleware(
AuthMiddlewareStack(inner)
)
application = ProtocolTypeRouter({ application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack( 'websocket': CustomMiddlewareStack(
URLRouter( URLRouter(
quest.routing.websocket_urlpatterns quest.routing.websocket_urlpatterns
) )

View File

@ -48,6 +48,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'titivillus.middleware.XRealIPMiddleware',
] ]
ROOT_URLCONF = 'titivillus.urls' ROOT_URLCONF = 'titivillus.urls'