Compare commits

..

5 Commits

Author SHA1 Message Date
f78d407d4c move DiceRoller to its own module
while I'm doing that, standardize the usage of raising exceptions when parsing goes wrong
2019-06-21 16:53:40 -05:00
8528152483 remove dice cthulhutech roll. hasn't been used in forever 2019-06-21 16:51:09 -05:00
f2fb0a26a4 remove unnecessary unicode_literal future imports, we py3 now 2019-06-21 15:23:33 -05:00
0f88715ffd remove unnecessary requirements-server.* 2019-06-21 10:06:17 -05:00
2f98a64cdd version bumps and migration to django 2.2 2019-06-21 10:05:40 -05:00
39 changed files with 405 additions and 559 deletions

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-02-23 02:25 # Generated by Django 1.10.5 on 2017-02-23 02:25
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-02-24 01:06 # Generated by Django 1.10.5 on 2017-02-24 01:06
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-02-24 01:12 # Generated by Django 1.10.5 on 2017-02-24 01:12
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -1,36 +1,28 @@
"""Roll dice when asked, intended for RPGs.""" """Roll dice when asked, intended for RPGs."""
# this breaks yacc, but ply might be happy in py3
#from __future__ import unicode_literals
import logging import logging
import math
import re import re
import random import random
from irc.client import NickMask from irc.client import NickMask
import ply.lex as lex from dice.roller import DiceRoller
import ply.yacc as yacc
from ircbot.lib import Plugin from ircbot.lib import Plugin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Dice(Plugin): class Dice(Plugin):
"""Roll simple or complex dice strings.""" """Roll simple or complex dice strings."""
def __init__(self):
"""Set up the plugin."""
super(Dice, self).__init__()
self.roller = DiceRoller()
def start(self): def start(self):
"""Set up the handlers.""" """Set up the handlers."""
self.roller = DiceRoller()
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!roll\s+(.*)$', self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!roll\s+(.*)$',
self.handle_roll, -20) self.handle_roll, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!ctech\s+(.*)$',
self.handle_ctech, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!random\s+(.*)$', self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!random\s+(.*)$',
self.handle_random, -20) self.handle_random, -20)
@ -38,9 +30,7 @@ class Dice(Plugin):
def stop(self): def stop(self):
"""Tear down handlers.""" """Tear down handlers."""
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_roll) self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_roll)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_ctech)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_random) self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_random)
super(Dice, self).stop() super(Dice, self).stop()
@ -68,368 +58,17 @@ class Dice(Plugin):
dicestr = match.group(1) dicestr = match.group(1)
logger.debug(event.recursing) logger.debug(event.recursing)
try:
reply_str = self.roller.do_roll(dicestr)
except AssertionError as aex:
reply_str = f"Could not roll dice: {aex}"
except ValueError:
reply_str = "Unable to parse roll"
if event.recursing: if event.recursing:
reply = "{0:s}".format(self.roller.do_roll(dicestr)) reply = "{0:s}".format(reply_str)
else: else:
reply = "{0:s}: {1:s}".format(nick, self.roller.do_roll(dicestr)) reply = "{0:s}: {1:s}".format(nick, reply_str)
return self.bot.reply(event, re.sub(r'(\d+)(.*?\s+)(\(.*?\))', r'\1\214\3', reply)) return self.bot.reply(event, re.sub(r'(\d+)(.*?\s+)(\(.*?\))', r'\1\214\3', reply))
def handle_ctech(self, connection, event, match):
"""Handle cthulhutech dice rolls."""
nick = NickMask(event.source).nick
rollitrs = re.split(';\s*', match.group(1))
reply = ""
for count, roll in enumerate(rollitrs):
pattern = '^(\d+)d(?:(\+|\-)(\d+))?(?:\s+(.*))?'
regex = re.compile(pattern)
matches = regex.search(roll)
if matches is not None:
dice = int(matches.group(1))
modifier = 0
if matches.group(2) is not None and matches.group(3) is not None:
if str(matches.group(2)) == '-':
modifier = -1 * int(matches.group(3))
else:
modifier = int(matches.group(3))
result = roll + ': '
rolls = []
for d in range(dice):
rolls.append(random.randint(1, 10))
rolls.sort()
rolls.reverse()
# highest single die method
method1 = rolls[0]
# highest set method
method2 = 0
rolling_sum = 0
for i, r in enumerate(rolls):
# if next roll is same as current, sum and continue, else see if sum is best so far
if i+1 < len(rolls) and rolls[i+1] == r:
if rolling_sum == 0:
rolling_sum = r
rolling_sum += r
else:
if rolling_sum > method2:
method2 = rolling_sum
rolling_sum = 0
# check for set in progress (e.g. lots of 1s)
if rolling_sum > method2:
method2 = rolling_sum
# straight method
method3 = 0
rolling_sum = 0
count = 0
for i, r in enumerate(rolls):
# if next roll is one less as current, sum and continue, else check len and see if sum is best so far
if i+1 < len(rolls) and rolls[i+1] == r-1:
if rolling_sum == 0:
rolling_sum = r
count += 1
rolling_sum += r-1
count += 1
else:
if count >= 3 and rolling_sum > method3:
method3 = rolling_sum
rolling_sum = 0
# check for straight in progress (e.g. straight ending in 1)
if count >= 3 and rolling_sum > method3:
method3 = rolling_sum
# get best roll
best = max([method1, method2, method3])
# check for critical failure
botch = False
ones = 0
for r in rolls:
if r == 1:
ones += 1
if ones >= math.ceil(float(len(rolls))/2):
botch = True
if botch:
result += 'BOTCH'
else:
result += str(best + modifier)
rollres = ''
for i,r in enumerate(rolls):
rollres += str(r)
if i is not len(rolls)-1:
rollres += ','
result += ' [' + rollres
if modifier != 0:
if modifier > 0:
result += ' +' + str(modifier)
else:
result += ' -' + str(modifier * -1)
result += ']'
reply += result
if count is not len(rollitrs)-1:
reply += "; "
if reply is not "":
msg = "{0:s}: {1:s}".format(nick, reply)
return self.bot.reply(event, msg)
class DiceRoller(object):
tokens = ['NUMBER', 'TEXT', 'ROLLSEP']
literals = ['#', '/', '+', '-', 'd']
t_TEXT = r'\s+[^;]+'
t_ROLLSEP = r';\s*'
def build(self):
lex.lex(module=self)
yacc.yacc(module=self)
def t_NUMBER(self, t):
r'\d+'
t.value = int(t.value)
return t
def t_error(self, t):
t.lexer.skip(1)
precedence = (
('left', 'ROLLSEP'),
('left', '+', '-'),
('right', 'd'),
('left', '#'),
('left', '/')
)
output = ""
def roll_dice(self, keep, dice, size):
"""Takes the parsed dice string for a single roll (eg 3/4d20) and performs
the actual roll. Returns a string representing the result.
"""
a = list(range(dice))
for i in range(dice):
a[i] = random.randint(1, size)
if keep != dice:
b = sorted(a, reverse=True)
b = b[0:keep]
else:
b = a
total = sum(b)
outstr = "[" + ",".join(str(i) for i in a) + "]"
return (total, outstr)
def process_roll(self, trials, mods, comment):
"""Processes rolls coming from the parser.
This generates the inputs for the roll_dice() command, and returns
the full string representing the whole current dice string (the part
up to a semicolon or end of line).
"""
output = ""
repeat = 1
if trials != None:
repeat = trials
for i in range(repeat):
mode = 1
total = 0
curr_str = ""
if i > 0:
output += ", "
for m in mods:
keep = 0
dice = 1
res = 0
# if m is a tuple, then it is a die roll
# m[0] = (keep, num dice)
# m[1] = num faces on the die
if type(m) == tuple:
if m[0] != None:
if m[0][0] != None:
keep = m[0][0]
dice = m[0][1]
size = m[1]
if keep > dice or keep == 0:
keep = dice
if size < 1:
output = "# of sides for die is incorrect: %d" % size
return output
if dice < 1:
output = "# of dice is incorrect: %d" % dice
return output
res = self.roll_dice(keep, dice, size)
curr_str += "%d%s" % (res[0], res[1])
res = res[0]
elif m == "+":
mode = 1
curr_str += "+"
elif m == "-":
mode = -1
curr_str += "-"
else:
res = m
curr_str += str(m)
total += mode * res
if repeat == 1:
if comment != None:
output = "%d %s (%s)" % (total, comment.strip(), curr_str)
else:
output = "%d (%s)" % (total, curr_str)
else:
output += "%d (%s)" % (total, curr_str)
if i == repeat - 1:
if comment != None:
output += " (%s)" % (comment.strip())
return output
def p_roll_r(self, p):
# Chain rolls together.
# General idea I had when creating this grammar: A roll string is a chain
# of modifiers, which may be repeated for a certain number of trials. It can
# have a comment that describes the roll
# Multiple roll strings can be chained with semicolon
'roll : roll ROLLSEP roll'
global output
p[0] = p[1] + "; " + p[3]
output = p[0]
def p_roll(self, p):
# Parse a basic roll string.
'roll : trial modifier comment'
global output
mods = []
if type(p[2]) == list:
mods = p[2]
else:
mods = [p[2]]
p[0] = self.process_roll(p[1], mods, p[3])
output = p[0]
def p_roll_no_trials(self, p):
# Parse a roll string without trials.
'roll : modifier comment'
global output
mods = []
if type(p[1]) == list:
mods = p[1]
else:
mods = [p[1]]
p[0] = self.process_roll(None, mods, p[2])
output = p[0]
def p_comment(self, p):
# Parse a comment.
'''comment : TEXT
|'''
if len(p) == 2:
p[0] = p[1]
else:
p[0] = None
def p_modifier(self, p):
# Parse a modifier on a roll string.
'''modifier : modifier "+" modifier
| modifier "-" modifier'''
# Use append to prevent nested lists (makes dealing with this easier)
if type(p[1]) == list:
p[1].append(p[2])
p[1].append(p[3])
p[0] = p[1]
elif type(p[3]) == list:
p[3].insert(0, p[2])
p[3].insert(0, p[1])
p[0] = p[3]
else:
p[0] = [p[1], p[2], p[3]]
def p_die(self, p):
# Return the left side before the "d", and the number of faces.
'modifier : left NUMBER'
p[0] = (p[1], p[2])
def p_die_num(self, p):
'modifier : NUMBER'
p[0] = p[1]
def p_left(self, p):
# Parse the number of dice we are rolling, and how many we are keeping.
'left : keep dice'
if p[1] == None:
p[0] = [None, p[2]]
else:
p[0] = [p[1], p[2]]
def p_left_all(self, p):
'left : dice'
p[0] = [None, p[1]]
def p_left_e(self, p):
'left :'
p[0] = None
def p_total(self, p):
'trial : NUMBER "#"'
if len(p) > 1:
p[0] = p[1]
else:
p[0] = None
def p_keep(self, p):
'keep : NUMBER "/"'
if p[1] != None:
p[0] = p[1]
else:
p[0] = None
def p_dice(self, p):
'dice : NUMBER "d"'
p[0] = p[1]
def p_dice_one(self, p):
'dice : "d"'
p[0] = 1
def p_error(self, p):
# Provide the user with something (albeit not much) when the roll can't be parsed.
global output
output = "Unable to parse roll"
def get_result(self):
global output
return output
def do_roll(self, dicestr):
"""
Roll some dice and get the result (with broken out rolls).
Keyword arguments:
dicestr - format:
N#X/YdS+M label
N#: do the following roll N times (optional)
X/: take the top X rolls of the Y times rolled (optional)
Y : roll the die specified Y times (optional, defaults to 1)
dS: roll a S-sided die
+M: add M to the result (-M for subtraction) (optional)
"""
self.build()
yacc.parse(dicestr)
return self.get_result()
plugin = Dice plugin = Dice

263
dice/roller.py Normal file
View File

@ -0,0 +1,263 @@
"""Dice rollers used by the views, bots, etc."""
import logging
import random
import ply.lex as lex
import ply.yacc as yacc
logger = logging.getLogger(__name__)
class DiceRoller(object):
tokens = ['NUMBER', 'TEXT', 'ROLLSEP']
literals = ['#', '/', '+', '-', 'd']
t_TEXT = r'\s+[^;]+'
t_ROLLSEP = r';\s*'
def build(self):
lex.lex(module=self)
yacc.yacc(module=self)
def t_NUMBER(self, t):
r'\d+'
t.value = int(t.value)
return t
def t_error(self, t):
t.lexer.skip(1)
precedence = (
('left', 'ROLLSEP'),
('left', '+', '-'),
('right', 'd'),
('left', '#'),
('left', '/')
)
output = ""
def roll_dice(self, keep, dice, size):
"""Takes the parsed dice string for a single roll (eg 3/4d20) and performs
the actual roll. Returns a string representing the result.
"""
a = list(range(dice))
for i in range(dice):
a[i] = random.randint(1, size)
if keep != dice:
b = sorted(a, reverse=True)
b = b[0:keep]
else:
b = a
total = sum(b)
outstr = "[" + ",".join(str(i) for i in a) + "]"
return (total, outstr)
def process_roll(self, trials, mods, comment):
"""Processes rolls coming from the parser.
This generates the inputs for the roll_dice() command, and returns
the full string representing the whole current dice string (the part
up to a semicolon or end of line).
"""
rolls = mods[0][0][1] if mods[0][0][1] else 1
assert trials <= 10, "Too many rolls (max: 10)."
assert rolls <= 50, "Too many dice (max: 50) in roll."
output = ""
repeat = 1
if trials != None:
repeat = trials
for i in range(repeat):
mode = 1
total = 0
curr_str = ""
if i > 0:
output += ", "
for m in mods:
keep = 0
dice = 1
res = 0
# if m is a tuple, then it is a die roll
# m[0] = (keep, num dice)
# m[1] = num faces on the die
if type(m) == tuple:
if m[0] != None:
if m[0][0] != None:
keep = m[0][0]
dice = m[0][1]
size = m[1]
if keep > dice or keep == 0:
keep = dice
if size < 1:
output = "# of sides for die is incorrect: %d" % size
return output
if dice < 1:
output = "# of dice is incorrect: %d" % dice
return output
res = self.roll_dice(keep, dice, size)
curr_str += "%d%s" % (res[0], res[1])
res = res[0]
elif m == "+":
mode = 1
curr_str += "+"
elif m == "-":
mode = -1
curr_str += "-"
else:
res = m
curr_str += str(m)
total += mode * res
if repeat == 1:
if comment != None:
output = "%d %s (%s)" % (total, comment.strip(), curr_str)
else:
output = "%d (%s)" % (total, curr_str)
else:
output += "%d (%s)" % (total, curr_str)
if i == repeat - 1:
if comment != None:
output += " (%s)" % (comment.strip())
return output
def p_roll_r(self, p):
# Chain rolls together.
# General idea I had when creating this grammar: A roll string is a chain
# of modifiers, which may be repeated for a certain number of trials. It can
# have a comment that describes the roll
# Multiple roll strings can be chained with semicolon
'roll : roll ROLLSEP roll'
global output
p[0] = p[1] + "; " + p[3]
output = p[0]
def p_roll(self, p):
# Parse a basic roll string.
'roll : trial modifier comment'
global output
mods = []
if type(p[2]) == list:
mods = p[2]
else:
mods = [p[2]]
p[0] = self.process_roll(p[1], mods, p[3])
output = p[0]
def p_roll_no_trials(self, p):
# Parse a roll string without trials.
'roll : modifier comment'
global output
mods = []
if type(p[1]) == list:
mods = p[1]
else:
mods = [p[1]]
p[0] = self.process_roll(None, mods, p[2])
output = p[0]
def p_comment(self, p):
# Parse a comment.
'''comment : TEXT
|'''
if len(p) == 2:
p[0] = p[1]
else:
p[0] = None
def p_modifier(self, p):
# Parse a modifier on a roll string.
'''modifier : modifier "+" modifier
| modifier "-" modifier'''
# Use append to prevent nested lists (makes dealing with this easier)
if type(p[1]) == list:
p[1].append(p[2])
p[1].append(p[3])
p[0] = p[1]
elif type(p[3]) == list:
p[3].insert(0, p[2])
p[3].insert(0, p[1])
p[0] = p[3]
else:
p[0] = [p[1], p[2], p[3]]
def p_die(self, p):
# Return the left side before the "d", and the number of faces.
'modifier : left NUMBER'
p[0] = (p[1], p[2])
def p_die_num(self, p):
'modifier : NUMBER'
p[0] = p[1]
def p_left(self, p):
# Parse the number of dice we are rolling, and how many we are keeping.
'left : keep dice'
if p[1] == None:
p[0] = [None, p[2]]
else:
p[0] = [p[1], p[2]]
def p_left_all(self, p):
'left : dice'
p[0] = [None, p[1]]
def p_left_e(self, p):
'left :'
p[0] = None
def p_total(self, p):
'trial : NUMBER "#"'
if len(p) > 1:
p[0] = p[1]
else:
p[0] = None
def p_keep(self, p):
'keep : NUMBER "/"'
if p[1] != None:
p[0] = p[1]
else:
p[0] = None
def p_dice(self, p):
'dice : NUMBER "d"'
p[0] = p[1]
def p_dice_one(self, p):
'dice : "d"'
p[0] = 1
def p_error(self, p):
"""Raise ValueError on unparseable strings."""
raise ValueError("Error occurred in parser")
def get_result(self):
global output
return output
def do_roll(self, dicestr):
"""
Roll some dice and get the result (with broken out rolls).
Keyword arguments:
dicestr - format:
N#X/YdS+M label
N#: do the following roll N times (optional)
X/: take the top X rolls of the Y times rolled (optional)
Y : roll the die specified Y times (optional, defaults to 1)
dS: roll a S-sided die
+M: add M to the result (-M for subtraction) (optional)
"""
self.build()
yacc.parse(dicestr)
return self.get_result()

View File

@ -29,6 +29,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='dispatcheraction', model_name='dispatcheraction',
name='dispatcher', name='dispatcher',
field=models.ForeignKey(to='dispatch.Dispatcher'), field=models.ForeignKey(to='dispatch.Dispatcher', on_delete=models.CASCADE),
), ),
] ]

View File

@ -17,6 +17,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='dispatcheraction', model_name='dispatcheraction',
name='dispatcher', name='dispatcher',
field=models.ForeignKey(related_name='actions', to='dispatch.Dispatcher'), field=models.ForeignKey(related_name='actions', to='dispatch.Dispatcher', on_delete=models.CASCADE),
), ),
] ]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -36,7 +36,7 @@ class DispatcherAction(models.Model):
(FILE_TYPE, "Write to file"), (FILE_TYPE, "Write to file"),
) )
dispatcher = models.ForeignKey('Dispatcher', related_name='actions') dispatcher = models.ForeignKey('Dispatcher', related_name='actions', on_delete=models.CASCADE)
type = models.CharField(max_length=16, choices=TYPE_CHOICES) type = models.CharField(max_length=16, choices=TYPE_CHOICES)
destination = models.CharField(max_length=200) destination = models.CharField(max_length=200)
include_key = models.BooleanField(default=False) include_key = models.BooleanField(default=False)

View File

@ -11,7 +11,7 @@ https://docs.djangoproject.com/en/1.6/ref/settings/
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os import os
from django.core.urlresolvers import reverse_lazy from django.urls import reverse_lazy
BASE_DIR = os.path.dirname(os.path.dirname(__file__)) BASE_DIR = os.path.dirname(os.path.dirname(__file__))
@ -57,12 +57,11 @@ INSTALLED_APPS = (
'twitter', 'twitter',
) )
MIDDLEWARE_CLASSES = ( MIDDLEWARE = (
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',

View File

@ -20,5 +20,5 @@ urlpatterns = [
url(r'^races/', include('races.urls')), url(r'^races/', include('races.urls')),
url(r'^accounts/', include('registration.backends.default.urls')), url(r'^accounts/', include('registration.backends.default.urls')),
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', admin.site.urls),
] ]

View File

@ -26,6 +26,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='fact', model_name='fact',
name='category', name='category',
field=models.ForeignKey(to='facts.FactCategory'), field=models.ForeignKey(to='facts.FactCategory', on_delete=models.CASCADE),
), ),
] ]

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-02-11 15:00 # Generated by Django 1.10.5 on 2017-02-11 15:00
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -52,7 +52,7 @@ class Fact(models.Model):
"""Define facts.""" """Define facts."""
fact = models.TextField() fact = models.TextField()
category = models.ForeignKey(FactCategory) category = models.ForeignKey(FactCategory, on_delete=models.CASCADE)
nickmask = models.CharField(max_length=200, default='', blank=True) nickmask = models.CharField(max_length=200, default='', blank=True)
time = models.DateTimeField(auto_now_add=True) time = models.DateTimeField(auto_now_add=True)

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
@ -27,7 +25,7 @@ class Migration(migrations.Migration):
('code_reviews_necessary', models.PositiveSmallIntegerField(default=0)), ('code_reviews_necessary', models.PositiveSmallIntegerField(default=0)),
('code_reviewers', models.TextField(blank=True, default='')), ('code_reviewers', models.TextField(blank=True, default='')),
('code_review_final_merge_assignees', models.TextField(blank=True, default='')), ('code_review_final_merge_assignees', models.TextField(blank=True, default='')),
('gitlab_config', models.ForeignKey(to='gitlab_bot.GitlabConfig', null=True)), ('gitlab_config', models.ForeignKey(to='gitlab_bot.GitlabConfig', null=True, on_delete=models.CASCADE)),
], ],
), ),
] ]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -23,7 +23,7 @@ class GitlabProjectConfig(models.Model):
"""Maintain settings for a particular project in GitLab.""" """Maintain settings for a particular project in GitLab."""
gitlab_config = models.ForeignKey('GitlabConfig', null=True) gitlab_config = models.ForeignKey('GitlabConfig', null=True, on_delete=models.CASCADE)
project_id = models.CharField(max_length=64, unique=True) project_id = models.CharField(max_length=64, unique=True)
manage_merge_request_code_reviews = models.BooleanField(default=False) manage_merge_request_code_reviews = models.BooleanField(default=False)

View File

@ -15,7 +15,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='botadmin', model_name='botadmin',
name='user', name='user',
field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL), field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
preserve_default=False, preserve_default=False,
), ),
] ]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -47,7 +47,7 @@ class BotUser(models.Model):
"""Configure bot users, which can do things through the bot and standard Django auth.""" """Configure bot users, which can do things through the bot and standard Django auth."""
nickmask = models.CharField(max_length=200, unique=True) nickmask = models.CharField(max_length=200, unique=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class Meta: class Meta:
permissions = ( permissions = (

View File

@ -23,7 +23,7 @@ class Migration(migrations.Migration):
('delta', models.SmallIntegerField()), ('delta', models.SmallIntegerField()),
('nickmask', models.CharField(default='', max_length=200, blank=True)), ('nickmask', models.CharField(default='', max_length=200, blank=True)),
('created', models.DateTimeField(auto_now_add=True)), ('created', models.DateTimeField(auto_now_add=True)),
('key', models.ForeignKey(to='karma.KarmaKey')), ('key', models.ForeignKey(to='karma.KarmaKey', on_delete=models.CASCADE)),
], ],
), ),
] ]

View File

@ -115,7 +115,7 @@ class KarmaLogEntryManager(models.Manager):
class KarmaLogEntry(models.Model): class KarmaLogEntry(models.Model):
"""Track each karma increment/decrement.""" """Track each karma increment/decrement."""
key = models.ForeignKey('KarmaKey') key = models.ForeignKey('KarmaKey', on_delete=models.CASCADE)
delta = models.SmallIntegerField() delta = models.SmallIntegerField()
nickmask = models.CharField(max_length=200, default='', blank=True) nickmask = models.CharField(max_length=200, default='', blank=True)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)

View File

@ -27,7 +27,7 @@ class Migration(migrations.Migration):
('k2', models.CharField(max_length=128)), ('k2', models.CharField(max_length=128)),
('v', models.CharField(max_length=128)), ('v', models.CharField(max_length=128)),
('count', models.IntegerField(default=0)), ('count', models.IntegerField(default=0)),
('context', models.ForeignKey(to='markov.MarkovContext')), ('context', models.ForeignKey(to='markov.MarkovContext', on_delete=models.CASCADE)),
], ],
options={ options={
'permissions': set([('teach_line', 'Can teach lines'), ('import_log_file', 'Can import states from a log file')]), 'permissions': set([('teach_line', 'Can teach lines'), ('import_log_file', 'Can import states from a log file')]),
@ -40,7 +40,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(unique=True, max_length=64)), ('name', models.CharField(unique=True, max_length=64)),
('chatter_chance', models.IntegerField(default=0)), ('chatter_chance', models.IntegerField(default=0)),
('context', models.ForeignKey(to='markov.MarkovContext')), ('context', models.ForeignKey(to='markov.MarkovContext', on_delete=models.CASCADE)),
], ],
options={ options={
}, },

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -28,7 +28,7 @@ class MarkovTarget(models.Model):
"""Define IRC targets that relate to a context, and can occasionally be talked to.""" """Define IRC targets that relate to a context, and can occasionally be talked to."""
name = models.CharField(max_length=200, unique=True) name = models.CharField(max_length=200, unique=True)
context = models.ForeignKey(MarkovContext) context = models.ForeignKey(MarkovContext, on_delete=models.CASCADE)
chatter_chance = models.IntegerField(default=0) chatter_chance = models.IntegerField(default=0)
@ -51,7 +51,7 @@ class MarkovState(models.Model):
v = models.CharField(max_length=128) v = models.CharField(max_length=128)
count = models.IntegerField(default=0) count = models.IntegerField(default=0)
context = models.ForeignKey(MarkovContext) context = models.ForeignKey(MarkovContext, on_delete=models.CASCADE)
class Meta: class Meta:
index_together = [ index_together = [

View File

@ -29,7 +29,7 @@ class Migration(migrations.Migration):
('joined', models.BooleanField(default=False)), ('joined', models.BooleanField(default=False)),
('started', models.BooleanField(default=False)), ('started', models.BooleanField(default=False)),
('finished', models.BooleanField(default=False)), ('finished', models.BooleanField(default=False)),
('race', models.ForeignKey(to='races.Race')), ('race', models.ForeignKey(to='races.Race', on_delete=models.CASCADE)),
], ],
options={ options={
}, },
@ -41,8 +41,8 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('update', models.TextField()), ('update', models.TextField()),
('event_time', models.DateTimeField(default=django.utils.timezone.now)), ('event_time', models.DateTimeField(default=django.utils.timezone.now)),
('race', models.ForeignKey(to='races.Race')), ('race', models.ForeignKey(to='races.Race', on_delete=models.CASCADE)),
('racer', models.ForeignKey(to='races.Racer')), ('racer', models.ForeignKey(to='races.Racer', on_delete=models.CASCADE)),
], ],
options={ options={
'ordering': ['event_time'], 'ordering': ['event_time'],

View File

@ -28,7 +28,7 @@ class Racer(models.Model):
"""Track a racer in a race.""" """Track a racer in a race."""
nick = models.CharField(max_length=64) nick = models.CharField(max_length=64)
race = models.ForeignKey(Race) race = models.ForeignKey(Race, on_delete=models.CASCADE)
joined = models.BooleanField(default=False) joined = models.BooleanField(default=False)
started = models.BooleanField(default=False) started = models.BooleanField(default=False)
@ -47,8 +47,8 @@ class RaceUpdate(models.Model):
"""Periodic updates for a racer.""" """Periodic updates for a racer."""
race = models.ForeignKey(Race) race = models.ForeignKey(Race, on_delete=models.CASCADE)
racer = models.ForeignKey(Racer) racer = models.ForeignKey(Racer, on_delete=models.CASCADE)
update = models.TextField() update = models.TextField()
event_time = models.DateTimeField(default=timezone.now) event_time = models.DateTimeField(default=timezone.now)

View File

@ -2,64 +2,68 @@
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile
# To update, run: # To update, run:
# #
# pip-compile --output-file requirements-dev.txt requirements-dev.in # pip-compile --output-file=requirements-dev.txt requirements-dev.in
# #
appdirs==1.4.0 # via setuptools astroid==2.2.5 # via pylint, pylint-celery, pylint-flask, requirements-detector
astroid==1.4.9 # via pylint, pylint-celery, pylint-flask, pylint-plugin-utils, requirements-detector certifi==2019.6.16 # via requests
click==6.7 # via pip-tools chardet==3.0.4 # via requests
click==7.0 # via pip-tools
django-adminplus==0.5 django-adminplus==0.5
django-bootstrap3==8.1.0 django-bootstrap3==11.0.0
django-extensions==1.7.6 django-extensions==2.1.9
django-registration-redux==1.4 django-registration-redux==2.6
django==1.10.5 django==2.2.2
djangorestframework==3.5.3 djangorestframework==3.9.4
dodgy==0.1.9 # via prospector dodgy==0.1.9 # via prospector
first==2.0.1 # via pip-tools future==0.17.1 # via parsedatetime
future==0.16.0 # via parsedatetime idna==2.8 # via requests
inflect==0.2.5 # via jaraco.itertools importlib-metadata==0.18 # via irc
irc==15.0.6 inflect==2.1.0 # via jaraco.itertools
isort==4.2.5 # via pylint irc==17.1
jaraco.classes==1.4 # via jaraco.collections isort==4.3.20 # via pylint
jaraco.collections==1.5 # via irc, jaraco.text jaraco.classes==2.0 # via jaraco.collections
jaraco.functools==1.15.1 # via irc, jaraco.text jaraco.collections==2.0 # via irc
jaraco.itertools==2.0 # via irc jaraco.functools==2.0 # via irc, jaraco.text, tempora
jaraco.logging==1.5 # via irc jaraco.itertools==4.4.2 # via irc
jaraco.stream==1.1.1 # via irc jaraco.logging==2.0 # via irc
jaraco.text==1.9 # via irc, jaraco.collections jaraco.stream==2.0 # via irc
lazy-object-proxy==1.2.2 # via astroid jaraco.text==3.0 # via irc, jaraco.collections
logilab-common==1.3.0 lazy-object-proxy==1.4.1 # via astroid
logilab-common==1.4.2
mccabe==0.6.1 # via prospector, pylint mccabe==0.6.1 # via prospector, pylint
more-itertools==2.5.0 # via irc, jaraco.functools, jaraco.itertools more-itertools==7.0.0 # via irc, jaraco.functools, jaraco.itertools
oauthlib==2.0.1 # via requests-oauthlib oauthlib==3.0.1 # via requests-oauthlib
packaging==16.8 # via setuptools parsedatetime==2.4
parsedatetime==2.2
pep8-naming==0.4.1 # via prospector pep8-naming==0.4.1 # via prospector
pip-tools==1.8.0 pip-tools==3.8.0
ply==3.10 ply==3.11
prospector==0.12.4 prospector==1.1.6.4
pycodestyle==2.0.0 # via prospector pycodestyle==2.4.0 # via prospector
pydocstyle==1.0.0 # via prospector pydocstyle==3.0.0 # via prospector
pyflakes==1.5.0 # via prospector pyflakes==1.6.0 # via prospector
pylint-celery==0.3 # via prospector pylint-celery==0.3 # via prospector
pylint-common==0.2.2 # via prospector pylint-django==2.0.9 # via prospector
pylint-django==0.7.2 # via prospector pylint-flask==0.6 # via prospector
pylint-flask==0.5 # via prospector pylint-plugin-utils==0.5 # via prospector, pylint-celery, pylint-django, pylint-flask
pylint-plugin-utils==0.2.4 # via prospector, pylint-celery, pylint-django, pylint-flask pylint==2.3.1 # via prospector, pylint-celery, pylint-django, pylint-flask, pylint-plugin-utils
pylint==1.6.5 # via prospector, pylint-celery, pylint-common, pylint-django, pylint-flask, pylint-plugin-utils python-dateutil==2.8.0
pyparsing==2.1.10 # via packaging python-gitlab==1.9.0
python-dateutil==2.6.0 python-mpd2==1.0.0
python-gitlab==0.18 pytz==2019.1
python-mpd2==0.5.5 pyyaml==5.1.1 # via prospector
pytz==2016.10 requests-oauthlib==1.2.0 # via twython
pyyaml==3.12 # via prospector requests==2.22.0 # via python-gitlab, requests-oauthlib, twython
requests-oauthlib==0.7.0 # via twython requirements-detector==0.6 # via prospector
requests==2.13.0 # via python-gitlab, requests-oauthlib, twython
requirements-detector==0.5.2 # via prospector
setoptconf==0.2.0 # via prospector setoptconf==0.2.0 # via prospector
six==1.10.0 # via astroid, django-extensions, irc, jaraco.classes, jaraco.collections, jaraco.itertools, jaraco.logging, jaraco.stream, logilab-common, more-itertools, packaging, pip-tools, pylint, python-dateutil, python-gitlab, setuptools, tempora six==1.12.0 # via astroid, django-extensions, jaraco.classes, jaraco.collections, jaraco.itertools, jaraco.logging, jaraco.stream, logilab-common, pip-tools, pydocstyle, python-dateutil, python-gitlab, tempora
tempora==1.6.1 # via irc, jaraco.logging snowballstemmer==1.2.1 # via pydocstyle
twython==3.4.0 sqlparse==0.3.0 # via django
wrapt==1.10.8 # via astroid tempora==1.14.1 # via irc, jaraco.logging
twython==3.7.0
typed-ast==1.4.0 # via astroid
urllib3==1.25.3 # via requests
wrapt==1.11.2 # via astroid
zipp==0.5.1 # via importlib-metadata
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:
# setuptools # via logilab-common # setuptools==41.0.1 # via logilab-common

View File

@ -1,3 +0,0 @@
-r requirements.in
psycopg2

View File

@ -1,36 +0,0 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file requirements-server.txt requirements-server.in
#
django-adminplus==0.5
django-bootstrap3==8.1.0
django-extensions==1.7.6
django-registration-redux==1.4
django==1.10.5
djangorestframework==3.5.3
future==0.16.0 # via parsedatetime
inflect==0.2.5 # via jaraco.itertools
irc==15.0.6
jaraco.classes==1.4 # via jaraco.collections
jaraco.collections==1.5 # via irc, jaraco.text
jaraco.functools==1.15.1 # via irc, jaraco.text
jaraco.itertools==2.0 # via irc
jaraco.logging==1.5 # via irc
jaraco.stream==1.1.1 # via irc
jaraco.text==1.9 # via irc, jaraco.collections
more-itertools==2.5.0 # via irc, jaraco.functools, jaraco.itertools
oauthlib==2.0.1 # via requests-oauthlib
parsedatetime==2.2
ply==3.10
psycopg2==2.6.2
python-dateutil==2.6.0
python-gitlab==0.18
python-mpd2==0.5.5
pytz==2016.10
requests-oauthlib==0.7.0 # via twython
requests==2.13.0 # via python-gitlab, requests-oauthlib, twython
six==1.10.0 # via django-extensions, irc, jaraco.classes, jaraco.collections, jaraco.itertools, jaraco.logging, jaraco.stream, more-itertools, python-dateutil, python-gitlab, tempora
tempora==1.6.1 # via irc, jaraco.logging
twython==3.4.0

View File

@ -2,34 +2,41 @@
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile
# To update, run: # To update, run:
# #
# pip-compile --output-file requirements.txt requirements.in # pip-compile --output-file=requirements.txt requirements.in
# #
certifi==2019.6.16 # via requests
chardet==3.0.4 # via requests
django-adminplus==0.5 django-adminplus==0.5
django-bootstrap3==8.1.0 django-bootstrap3==11.0.0
django-extensions==1.7.6 django-extensions==2.1.9
django-registration-redux==1.4 django-registration-redux==2.6
django==1.10.5 django==2.2.2
djangorestframework==3.5.3 djangorestframework==3.9.4
future==0.16.0 # via parsedatetime future==0.17.1 # via parsedatetime
inflect==0.2.5 # via jaraco.itertools idna==2.8 # via requests
irc==15.0.6 importlib-metadata==0.18 # via irc
jaraco.classes==1.4 # via jaraco.collections inflect==2.1.0 # via jaraco.itertools
jaraco.collections==1.5 # via irc, jaraco.text irc==17.1
jaraco.functools==1.15.1 # via irc, jaraco.text jaraco.classes==2.0 # via jaraco.collections
jaraco.itertools==2.0 # via irc jaraco.collections==2.0 # via irc
jaraco.logging==1.5 # via irc jaraco.functools==2.0 # via irc, jaraco.text, tempora
jaraco.stream==1.1.1 # via irc jaraco.itertools==4.4.2 # via irc
jaraco.text==1.9 # via irc, jaraco.collections jaraco.logging==2.0 # via irc
more-itertools==2.5.0 # via irc, jaraco.functools, jaraco.itertools jaraco.stream==2.0 # via irc
oauthlib==2.0.1 # via requests-oauthlib jaraco.text==3.0 # via irc, jaraco.collections
parsedatetime==2.2 more-itertools==7.0.0 # via irc, jaraco.functools, jaraco.itertools
ply==3.10 oauthlib==3.0.1 # via requests-oauthlib
python-dateutil==2.6.0 parsedatetime==2.4
python-gitlab==0.18 ply==3.11
python-mpd2==0.5.5 python-dateutil==2.8.0
pytz==2016.10 python-gitlab==1.9.0
requests-oauthlib==0.7.0 # via twython python-mpd2==1.0.0
requests==2.13.0 # via python-gitlab, requests-oauthlib, twython pytz==2019.1
six==1.10.0 # via django-extensions, irc, jaraco.classes, jaraco.collections, jaraco.itertools, jaraco.logging, jaraco.stream, more-itertools, python-dateutil, python-gitlab, tempora requests-oauthlib==1.2.0 # via twython
tempora==1.6.1 # via irc, jaraco.logging requests==2.22.0 # via python-gitlab, requests-oauthlib, twython
twython==3.4.0 six==1.12.0 # via django-extensions, jaraco.classes, jaraco.collections, jaraco.itertools, jaraco.logging, jaraco.stream, python-dateutil, python-gitlab, tempora
sqlparse==0.3.0 # via django
tempora==1.14.1 # via irc, jaraco.logging
twython==3.7.0
urllib3==1.25.3 # via requests
zipp==0.5.1 # via importlib-metadata

View File

@ -1,7 +1,4 @@
"""Use dr.botzo's Dispatch to send mpd notifications.""" """Use dr.botzo's Dispatch to send mpd notifications."""
from __future__ import unicode_literals
import argparse import argparse
import getpass import getpass
import logging import logging

View File

@ -29,7 +29,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('line', models.TextField(default='')), ('line', models.TextField(default='')),
('time', models.DateTimeField(auto_now_add=True)), ('time', models.DateTimeField(auto_now_add=True)),
('game', models.ForeignKey(to='storycraft.StorycraftGame')), ('game', models.ForeignKey(to='storycraft.StorycraftGame', on_delete=models.CASCADE)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
@ -38,12 +38,12 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('nick', models.CharField(max_length=64)), ('nick', models.CharField(max_length=64)),
('nickmask', models.CharField(max_length=200)), ('nickmask', models.CharField(max_length=200)),
('game', models.ForeignKey(to='storycraft.StorycraftGame')), ('game', models.ForeignKey(to='storycraft.StorycraftGame', on_delete=models.CASCADE)),
], ],
), ),
migrations.AddField( migrations.AddField(
model_name='storycraftline', model_name='storycraftline',
name='player', name='player',
field=models.ForeignKey(to='storycraft.StorycraftPlayer'), field=models.ForeignKey(to='storycraft.StorycraftPlayer', on_delete=models.CASCADE),
), ),
] ]

View File

@ -13,16 +13,16 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='storycraftline', model_name='storycraftline',
name='game', name='game',
field=models.ForeignKey(related_name='lines', to='storycraft.StorycraftGame'), field=models.ForeignKey(related_name='lines', to='storycraft.StorycraftGame', on_delete=models.CASCADE),
), ),
migrations.AlterField( migrations.AlterField(
model_name='storycraftline', model_name='storycraftline',
name='player', name='player',
field=models.ForeignKey(related_name='lines', to='storycraft.StorycraftPlayer'), field=models.ForeignKey(related_name='lines', to='storycraft.StorycraftPlayer', on_delete=models.CASCADE),
), ),
migrations.AlterField( migrations.AlterField(
model_name='storycraftplayer', model_name='storycraftplayer',
name='game', name='game',
field=models.ForeignKey(related_name='players', to='storycraft.StorycraftGame'), field=models.ForeignKey(related_name='players', to='storycraft.StorycraftGame', on_delete=models.CASCADE),
), ),
] ]

View File

@ -89,7 +89,7 @@ class StorycraftPlayer(models.Model):
"""Contain entire games of storycraft.""" """Contain entire games of storycraft."""
game = models.ForeignKey('StorycraftGame', related_name='players') game = models.ForeignKey('StorycraftGame', related_name='players', on_delete=models.CASCADE)
nick = models.CharField(max_length=64) nick = models.CharField(max_length=64)
nickmask = models.CharField(max_length=200) nickmask = models.CharField(max_length=200)
@ -103,8 +103,8 @@ class StorycraftLine(models.Model):
"""Handle requests to dispatchers and do something with them.""" """Handle requests to dispatchers and do something with them."""
game = models.ForeignKey('StorycraftGame', related_name='lines') game = models.ForeignKey('StorycraftGame', related_name='lines', on_delete=models.CASCADE)
player = models.ForeignKey('StorycraftPlayer', related_name='lines') player = models.ForeignKey('StorycraftPlayer', related_name='lines', on_delete=models.CASCADE)
line = models.TextField(default="") line = models.TextField(default="")
time = models.DateTimeField(auto_now_add=True) time = models.DateTimeField(auto_now_add=True)

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
@ -19,6 +17,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='twitterclient', model_name='twitterclient',
name='mentions_output_channel', name='mentions_output_channel',
field=models.ForeignKey(blank=True, related_name='mentions_twitter_client', null=True, to='ircbot.IrcChannel', default=None), field=models.ForeignKey(blank=True, related_name='mentions_twitter_client', null=True, to='ircbot.IrcChannel', default=None, on_delete=models.CASCADE),
), ),
] ]

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models

View File

@ -18,7 +18,7 @@ class TwitterClient(models.Model):
oauth_token_secret = models.CharField(max_length=256, default='', blank=True) oauth_token_secret = models.CharField(max_length=256, default='', blank=True)
mentions_output_channel = models.ForeignKey(IrcChannel, related_name='mentions_twitter_client', default=None, mentions_output_channel = models.ForeignKey(IrcChannel, related_name='mentions_twitter_client', default=None,
null=True, blank=True) null=True, blank=True, on_delete=models.CASCADE)
mentions_since_id = models.PositiveIntegerField(default=1, blank=True) mentions_since_id = models.PositiveIntegerField(default=1, blank=True)
class Meta: class Meta: