Punishments from db

This commit is contained in:
Johnny Gear 2026-03-04 21:01:35 -06:00
parent 18b9c9de55
commit 25df720757
12 changed files with 322 additions and 60 deletions

View file

@ -40,6 +40,14 @@ class OrdersPool(BaseModel):
time = TextField(null=True) time = TextField(null=True)
confirm_delay = IntegerField(null=True) confirm_delay = IntegerField(null=True)
punishment_pool = ForeignKeyField(
'self',
column_name='punishment_pool_id',
field='id',
backref='punishments_for',
null=True
)
def to_dict(self): def to_dict(self):
return { return {
'id': self.id, 'id': self.id,
@ -50,6 +58,7 @@ class OrdersPool(BaseModel):
'weekends': self.weekends, 'weekends': self.weekends,
'time': self.time, 'time': self.time,
'confirm_delay': self.confirm_delay, 'confirm_delay': self.confirm_delay,
'punishment_pool_id': self.punishment_pool_id,
'orders': [{ 'orders': [{
'id': order.id, 'id': order.id,
'name': order.name, 'name': order.name,
@ -90,19 +99,10 @@ class OrderAddOn(BaseModel):
# #
# Order State # Order State
# #
class PunishmentStatus(BaseModel):
confirmed_at = DateTimeField(null=True) # TIMESTAMP
created_at = DateTimeField() # TIMESTAMP
mastodon_id = TextField()
text = TextField()
class Meta:
table_name = 'punishment_status'
class OrderStatus(BaseModel): class OrderStatus(BaseModel):
confirmed_at = DateTimeField(null=True) # TIMESTAMP confirmed_at = DateTimeField(null=True) # TIMESTAMP
created_at = DateTimeField() # TIMESTAMP created_at = DateTimeField() # TIMESTAMP
due_at = DateTimeField() # TIMESTAMP due_at = DateTimeField(null=True) # TIMESTAMP
mastodon_id = TextField() mastodon_id = TextField()
text = TextField() text = TextField()
@ -122,6 +122,14 @@ class OrderStatus(BaseModel):
null=False null=False
) )
punishment_for = ForeignKeyField(
'self',
column_name='punishment_for_id',
field='id',
backref='punishment',
null=True
)
class Meta: class Meta:
table_name = 'order_status' table_name = 'order_status'

View file

@ -1,14 +1,13 @@
import datetime import datetime
from .models import database, User, OrdersPool, DomSubUsers, Repeat, SkipDay, OrderStatus, PunishmentStatus from .models import database, User, OrdersPool, DomSubUsers, Repeat, SkipDay, OrderStatus
def initdb(): def initdb():
database.connect() database.connect()
database.create_tables([ database.create_tables([
Repeat, Repeat,
SkipDay, SkipDay,
OrderStatus, OrderStatus
PunishmentStatus
]) ])
def user_add(username, chat_id): def user_add(username, chat_id):
@ -90,24 +89,20 @@ def skip_day_contains(date):
q = SkipDay.select().where(SkipDay.date == date) q = SkipDay.select().where(SkipDay.date == date)
return len(q) > 0 return len(q) > 0
def order_status_put(orders_pool, user, mastodon_id, created_at, due_at, text): def order_status_put(orders_pool, user, mastodon_id, created_at, due_at, text, punishment_for=None):
return OrderStatus.create( return OrderStatus.create(
orders_pool_id=orders_pool.id, orders_pool_id=orders_pool.id,
user_id=user.id, user_id=user.id,
mastodon_id=mastodon_id, mastodon_id=mastodon_id,
created_at=created_at, created_at=created_at,
due_at=due_at, due_at=due_at,
text=text text=text,
punishment_for=punishment_for
) )
def order_status_by_id(order_status_id): def order_status_by_id(order_status_id):
return OrderStatus.get(id=order_status_id) return OrderStatus.get(id=order_status_id)
def order_status_outstanding():
return OrderStatus.select().where(
(OrderStatus.confirmed_at.is_null()) & (OrderStatus.punishment_id.is_null())
)
def order_status_confirm(id, confirmed_at): def order_status_confirm(id, confirmed_at):
q = OrderStatus.update( q = OrderStatus.update(
confirmed_at=confirmed_at confirmed_at=confirmed_at
@ -115,17 +110,3 @@ def order_status_confirm(id, confirmed_at):
OrderStatus.id == id OrderStatus.id == id
) )
return q.execute() return q.execute()
def punishment_status_put(order_status_id, mastodon_id, created_at, text):
punishment_status = PunishmentStatus.create(
mastodon_id=mastodon_id,
created_at=created_at,
text=text
)
q = OrderStatus.update(
punishment_id=punishment_status.id
).where(
OrderStatus.id == order_status_id
)
q.execute()

View file

@ -152,6 +152,14 @@ def sub_order_set(username, set_id, sub):
try: try:
op.name = request.json['name'] op.name = request.json['name']
if ('punishment_pool_id' in request.json and
request.json['punishment_pool_id'] in [
op.id for op in orders_pool_list(sub.id)
]):
op.punishment_pool_id = request.json['punishment_pool_id']
else:
op.punishment_pool_id = None
op.scheduled = request.json['scheduled'] op.scheduled = request.json['scheduled']
if op.scheduled: if op.scheduled:
op.probability = request.json['probability'] op.probability = request.json['probability']
@ -198,4 +206,14 @@ def sub_order_set(username, set_id, sub):
op.delete_instance(recursive=True) op.delete_instance(recursive=True)
return ('', 204) return ('', 204)
return jsonify(op.to_dict()) return jsonify({
"orderSets": [
{
'id': op.id,
'name': op.name,
}
for op
in orders_pool_list(sub.id)
],
"orderSet": op.to_dict()
})

View file

@ -15,6 +15,7 @@ import {
Collapse, Collapse,
NumberInput, NumberInput,
Text, Text,
Select,
} from "@mantine/core"; } from "@mantine/core";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { TimeInput } from "@mantine/dates"; import { TimeInput } from "@mantine/dates";
@ -68,21 +69,38 @@ type FormOrderSetOrder = Omit<OrderSetOrder, "id" | "add_ons"> & {
add_ons: FormOrderSetOrderAddOn[]; add_ons: FormOrderSetOrderAddOn[];
}; };
type FormOrderSet = Omit<OrderSet, "orders"> & { type FormOrderSet = Omit<OrderSet, "orders" | "punishment_pool_id"> & {
orders: FormOrderSetOrder[]; orders: FormOrderSetOrder[];
punishment_pool_id?: string | number;
}; };
export const OrderSet: React.FC = () => { export const OrderSet: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { username, set_id } = useParams(); const { username, set_id } = useParams();
const orderSet = useLoaderData<OrderSet>(); const { orderSets, orderSet } = useLoaderData<{
orderSets: Pick<OrderSet, "id" | "name">[];
orderSet: OrderSet;
}>();
const [showScheduling, setShowScheduling] = React.useState( const [showScheduling, setShowScheduling] = React.useState(
orderSet?.scheduled, orderSet?.scheduled,
); );
const punishmentOptions = React.useMemo(
() =>
orderSets.map(({ id, name }) => ({ id, value: `${id}`, label: name })),
[orderSets],
);
const form = useForm<Partial<FormOrderSet>>({ const form = useForm<Partial<FormOrderSet>>({
mode: "uncontrolled", mode: "uncontrolled",
initialValues: orderSet ?? { initialValues: orderSet
? {
...orderSet,
punishment_pool_id: orderSet.punishment_pool_id
? `${orderSet.punishment_pool_id}`
: undefined,
}
: {
scheduled: false, scheduled: false,
orders: [], orders: [],
}, },
@ -105,12 +123,22 @@ export const OrderSet: React.FC = () => {
value.length < 1 ? "Please enter a name" : null, value.length < 1 ? "Please enter a name" : null,
}, },
}, },
punishment_pool_id: (value: string) =>
!value || orderSets.some(({ id }) => id === Number(value))
? null
: "Please select a valid pool",
}, },
onValuesChange: (values) => { onValuesChange: (values) => {
if (values.scheduled !== showScheduling) { if (values.scheduled !== showScheduling) {
setShowScheduling(values.scheduled); setShowScheduling(values.scheduled);
} }
}, },
transformValues: (values) => ({
...values,
punishment_pool_id: values.punishment_pool_id
? Number(values.punishment_pool_id)
: undefined,
}),
}); });
const handleSubmit = React.useCallback( const handleSubmit = React.useCallback(
@ -235,6 +263,13 @@ export const OrderSet: React.FC = () => {
/> />
</Paper> </Paper>
</Collapse> </Collapse>
<Select
clearable
label="Punishment Pool"
description="Orders Pool to issue punishments from"
data={punishmentOptions}
{...form.getInputProps("punishment_pool_id")}
/>
<Title order={2} mt="lg" mb="sm"> <Title order={2} mt="lg" mb="sm">
Orders Orders

View file

@ -22,4 +22,5 @@ type OrderSet = {
weekends: boolean; weekends: boolean;
time: string; time: string;
confirm_delay: string; confirm_delay: string;
punishment_pool_id?: number;
} }

View file

@ -59,9 +59,10 @@ def generate_order(orders_pool):
"orders": result "orders": result
} }
def generate_punishment(): def generate_punishment(orders_pool):
orders_config = read_config() # Pick new orders
(result, repeat_p) = pick_order(orders_pool.orders)
(result, repeat_p) = pick_order(orders_config['punishments']) return {
"orders": result
return result }

View file

@ -0,0 +1,55 @@
"""Peewee migrations -- 009_delete_punishment_status.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.remove_model('punishment_status')
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
@migrator.create_model
class PunishmentStatus(pw.Model):
id = pw.AutoField()
confirmed_at = pw.DateTimeField(null=True)
created_at = pw.DateTimeField()
mastodon_id = pw.TextField()
text = pw.TextField()
class Meta:
table_name = "punishment_status"

View file

@ -0,0 +1,51 @@
"""Peewee migrations -- 010_add_order_status_punishment_for.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
'order_status',
punishment_for=pw.ForeignKeyField(column_name='punishment_for_id', field='id', model='self', null=True))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields('order_status', 'punishment_for')
migrator.drop_index('order_status', 'punishment_for')

View file

@ -0,0 +1,51 @@
"""Peewee migrations -- 011_add_orders_pool_punishment_pool.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
'orders_pool',
punishment_pool=pw.ForeignKeyField(column_name='punishment_pool_id', field='id', model='self', null=True))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields('orders_pool', 'punishment_pool')
migrator.drop_index('orders_pool', 'punishment_pool')

View file

@ -0,0 +1,50 @@
"""Peewee migrations -- 012_add_order_status_null_due_at.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.change_fields('order_status', due_at=pw.DateTimeField(null=True))
migrator.drop_not_null('order_status', 'due_at')
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.change_fields('order_status', due_at=pw.DateTimeField())
migrator.add_not_null('order_status', 'due_at')

View file

@ -4,7 +4,7 @@ import asyncio
from util import make_session from util import make_session
from generate import generate_order, generate_punishment from generate import generate_order, generate_punishment
from db.queries import order_status_by_id, order_status_put, punishment_status_put, order_status_outstanding, order_status_confirm from db.queries import order_status_by_id, order_status_put, order_status_confirm
from mastodon import Mastodon from mastodon import Mastodon
from telegram.telegram import Telegram from telegram.telegram import Telegram
from settings import MASTODON_USERNAME, ORDER_TIMEOUT, ENV from settings import MASTODON_USERNAME, ORDER_TIMEOUT, ENV
@ -91,6 +91,7 @@ async def order_issue(orders_pool):
) )
async def punishment_mastodon_post(session, punishment_str, reply_id=None): async def punishment_mastodon_post(session, punishment_str, reply_id=None):
# TODO: Get user's mastodon username
post = "@%s has failed to post proof of compliance. Here is the punishment -\n\n" % MASTODON_USERNAME post = "@%s has failed to post proof of compliance. Here is the punishment -\n\n" % MASTODON_USERNAME
post += punishment_str + "\n\n" post += punishment_str + "\n\n"
if ENV == 'dev': if ENV == 'dev':
@ -104,7 +105,7 @@ async def punishment_mastodon_post(session, punishment_str, reply_id=None):
in_reply_to_id=reply_id in_reply_to_id=reply_id
) )
async def punishment_telegram_post(session, punishment_str, m_url): async def punishment_telegram_post(session, orders_pool, punishment_str, m_url):
post = "You failed to show proof of compliance. Here is your punishment -\n\n" post = "You failed to show proof of compliance. Here is your punishment -\n\n"
post += punishment_str + "\n\n" post += punishment_str + "\n\n"
post += m_url post += m_url
@ -112,34 +113,40 @@ async def punishment_telegram_post(session, punishment_str, m_url):
post += "\n\n⚠️ DEV" post += "\n\n⚠️ DEV"
t = Telegram(session) t = Telegram(session)
await t.message_send(post) await t.message_send(orders_pool.user.telegram_chat_id, post)
async def punishment_issue(session, outstanding_order): async def punishment_issue(session, order_status):
# TODO: Generate a punishment if order_status.pool is None or order_status.pool.punishment_pool is None:
logger.info(f'Unable to issue a punishment for {order_status.id}, no punishment pool for order pool {order_status.pool.name}')
logger.info('TODO: Generate a punishment') # TODO: No punishment mastodon/telegram posts
return return
punishment = generate_punishment() punishment_pool = order_status.pool.punishment_pool
punishment_str = "\n".join(punishment)
punishment = generate_punishment(punishment_pool)
punishment_str = "\n".join(punishment['orders'])
punishment_status = await punishment_mastodon_post( punishment_status = await punishment_mastodon_post(
session, session,
punishment_str, punishment_str,
outstanding_order.mastodon_id, order_status.mastodon_id,
) )
await punishment_telegram_post( await punishment_telegram_post(
session, session,
punishment_pool,
punishment_str, punishment_str,
punishment_status['url'] punishment_status['url']
) )
punishment_status_put( order_status_put(
outstanding_order.id, punishment_pool,
order_status.user,
punishment_status['id'], punishment_status['id'],
punishment_status['created_at'], punishment_status['created_at'],
punishment_str None,
punishment_str,
punishment_for=order_status
) )
order_check_lock = asyncio.Lock() order_check_lock = asyncio.Lock()
@ -148,6 +155,10 @@ async def order_check(order_status_id):
async with make_session() as session: async with make_session() as session:
order_status = order_status_by_id(order_status_id) order_status = order_status_by_id(order_status_id)
if order_status.punishment.count() > 0:
logger.info(f'Punishment already issued for {order_status.id}')
return
m = Mastodon(session) m = Mastodon(session)
context = await m.statusContext(order_status.mastodon_id) context = await m.statusContext(order_status.mastodon_id)

View file

@ -6,7 +6,7 @@ from scheduler.asyncio import Scheduler
from settings import TIMEZONE from settings import TIMEZONE
from orders import order_issue, order_check from orders import order_issue, order_check
from db.queries import order_status_outstanding, orders_pool_by_id, orders_pool_scheduled, skip_day_contains from db.queries import orders_pool_by_id, orders_pool_scheduled, skip_day_contains
from util import order_time from util import order_time
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)