Compare commits

..

No commits in common. "2089ee4161a7e70e4938f422db954885d9d5a8d6" and "70fd59a4be0426d809adc1b74e969a7c296aa158" have entirely different histories.

30 changed files with 442 additions and 1058 deletions

View file

@ -6,9 +6,3 @@ TIMELINE_ORDER_PUNISHED = "ORDER_PUNISHED"
TIMELINE_ORDERS_POOL_CREATED = "ORDERS_POOL_CREATED" TIMELINE_ORDERS_POOL_CREATED = "ORDERS_POOL_CREATED"
TIMELINE_ORDERS_POOL_UPDATED = "ORDERS_POOL_UPDATED" TIMELINE_ORDERS_POOL_UPDATED = "ORDERS_POOL_UPDATED"
TIMELINE_ORDERS_POOL_DELETED = "ORDERS_POOL_DELETED" TIMELINE_ORDERS_POOL_DELETED = "ORDERS_POOL_DELETED"
TIMELINE_ORDERS_POOL_EVENTS = [
TIMELINE_ORDERS_POOL_CREATED,
TIMELINE_ORDERS_POOL_UPDATED,
TIMELINE_ORDERS_POOL_DELETED
]

View file

@ -40,10 +40,6 @@ class User(BaseModel):
verify_mastodon_favorite = BooleanField(null=False, default=False) verify_mastodon_favorite = BooleanField(null=False, default=False)
verify_delay = IntegerField(null=True) verify_delay = IntegerField(null=True)
permission_orders_pools_view = BooleanField(null=False, default=True)
permission_orders_pools_details = BooleanField(null=False, default=True)
permission_orders_pools_edit = BooleanField(null=False, default=True)
def mastodon_account(self): def mastodon_account(self):
if self.mastodon_server is None or self.mastodon_username is None: if self.mastodon_server is None or self.mastodon_username is None:
return return
@ -187,9 +183,6 @@ class OrderStatus(BaseModel):
null=True null=True
) )
def __str__(self):
return f"{self.pool} {self.id}"
class Meta: class Meta:
table_name = 'order_status' table_name = 'order_status'

View file

@ -2,7 +2,6 @@ import datetime
import json import json
from peewee import JOIN, fn from peewee import JOIN, fn
from db.constants import TIMELINE_ORDERS_POOL_EVENTS
from util import sqlite_time from util import sqlite_time
from .models import database, User, OrdersPool, DomSubUsers, Repeat, SkipDay, OrderStatus, MastodonServer, TimelineEvent from .models import database, User, OrdersPool, DomSubUsers, Repeat, SkipDay, OrderStatus, MastodonServer, TimelineEvent
@ -55,45 +54,6 @@ def user_preferences_set(id, mastodon_post_public, mastodon_attn_list):
def user_has_doms(id): def user_has_doms(id):
return DomSubUsers.select().where(DomSubUsers.sub_id == id).count() > 0 return DomSubUsers.select().where(DomSubUsers.sub_id == id).count() > 0
def user_doms(id):
return [d.dom for d in DomSubUsers.select(DomSubUsers.dom).where(DomSubUsers.sub_id == id)]
def user_subs(id):
return [d.sub for d in DomSubUsers.select(DomSubUsers.sub).where(DomSubUsers.dom_id == id)]
def user_can_orders_pools_view(user, sub):
doms = user_doms(sub.id)
if len(doms) > 0:
if user == sub:
return user.permission_orders_pools_view
else:
return user in doms
else:
return user.id == sub.id
def user_can_orders_pools_details(user, sub):
doms = user_doms(sub.id)
if len(doms) > 0:
if user == sub:
return user.permission_orders_pools_details
else:
return user in doms
else:
return user.id == sub.id
def user_can_orders_pools_edit(user, sub):
doms = user_doms(sub.id)
if len(doms) > 0:
if user == sub:
return user.permission_orders_pools_edit
else:
return user in doms
else:
return user.id == sub.id
def mastodon_server_get(name): def mastodon_server_get(name):
return MastodonServer.get(name=name) return MastodonServer.get(name=name)
@ -226,22 +186,13 @@ def timeline_event_put(type, text, user, orders_pool=None, order_status=None, ac
extra=json.dumps(extra) if extra is not None else None extra=json.dumps(extra) if extra is not None else None
) )
def timeline_event_recent(user_id, limit=5): def timeline_event_recent(user_ids, actor_ids=None):
user = User.get_by_id(user_id) return (TimelineEvent
.select()
can_view_orders_pools = user_can_orders_pools_view(user, user) .where((
TimelineEvent.user_id.in_(user_ids) |
result = TimelineEvent.select() (TimelineEvent.actor_user_id.in_(actor_ids) if actor_ids is not None else True)
))
if(can_view_orders_pools): .order_by(TimelineEvent.updated_at.desc())
result = result.where( .limit(10)
(TimelineEvent.user_id == user_id) |
(TimelineEvent.actor_user_id == user_id)
) )
else:
result = result.where(
((TimelineEvent.user_id == user_id) & TimelineEvent.type.not_in(TIMELINE_ORDERS_POOL_EVENTS)) |
(TimelineEvent.actor_user_id == user_id)
)
return result.order_by(TimelineEvent.updated_at.desc()).limit(limit)

40
main.py
View file

@ -7,24 +7,38 @@ import json
from db.constants import TIMELINE_ORDER_ISSUED, TIMELINE_ORDER_NOT_ISSUED from db.constants import TIMELINE_ORDER_ISSUED, TIMELINE_ORDER_NOT_ISSUED
from scheduling import OrderScheduler from scheduling import OrderScheduler
from generate import generate_order from generate import generate_order
from orders import order_issue, order_check, punishment_issue from orders import order_issue, order_check
from db.queries import orders_pool_by_id, order_status_by_id from db.queries import orders_pool_by_id, timeline_event_put
from telegram.telegram import handle_commands from telegram.telegram import handle_commands
from telegram.commands import commands from telegram.commands import commands
from util import make_session
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def do_order_issue(orders_pool_id): async def do_order_issue(orders_pool_id):
orders_pool = orders_pool_by_id(orders_pool_id) orders_pool = orders_pool_by_id(orders_pool_id)
await order_issue(orders_pool) issue_result = await order_issue(orders_pool)
async def do_punishment_issue(order_status_id): if 'order_status' in issue_result:
order_status = order_status_by_id(order_status_id) order_status = issue_result['order_status']
logger.info(f'Issued order id {order_status.id}')
async with make_session() as session: timeline_event_put(
await punishment_issue(session, order_status) TIMELINE_ORDER_ISSUED,
order_status.text.split("\n")[0],
user=orders_pool.user,
orders_pool=orders_pool,
order_status=order_status,
extra={
"mastodon_status_url": issue_result['mastodon_status']['url']
}
)
elif 'reason' in issue_result:
timeline_event_put(
TIMELINE_ORDER_NOT_ISSUED,
issue_result['reason'],
user=orders_pool.user,
orders_pool=orders_pool
)
if __name__=='__main__': if __name__=='__main__':
logging.basicConfig( logging.basicConfig(
@ -47,9 +61,6 @@ if __name__=='__main__':
parser_issue = subparsers.add_parser('check', help='Check on the status of an order') parser_issue = subparsers.add_parser('check', help='Check on the status of an order')
parser_issue.add_argument('order_status_id') parser_issue.add_argument('order_status_id')
parser_punish = subparsers.add_parser('punish', help="Punish an order")
parser_punish.add_argument('order_status_id')
args = parser.parse_args() args = parser.parse_args()
if args.command == 'generate': if args.command == 'generate':
@ -66,11 +77,6 @@ if __name__=='__main__':
loop.run_until_complete( loop.run_until_complete(
order_check(args.order_status_id) order_check(args.order_status_id)
) )
elif args.command == 'punish':
loop = asyncio.new_event_loop()
loop.run_until_complete(
do_punishment_issue(args.order_status_id)
)
else: else:
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
s = OrderScheduler(loop) s = OrderScheduler(loop)

View file

@ -1,51 +0,0 @@
"""Peewee migrations -- 029_add_sub_permissions.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(
'user',
permission_orders_pools_view=pw.BooleanField(default=True),
permission_orders_pools_details=pw.BooleanField(default=True),
permission_orders_pools_edit=pw.BooleanField(default=True))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields('user', 'permission_orders_pools_view', 'permission_orders_pools_details', 'permission_orders_pools_edit')

148
orders.py
View file

@ -2,7 +2,7 @@ import logging
import datetime import datetime
import asyncio import asyncio
from db.constants import TIMELINE_ORDER_CONFIRMED, TIMELINE_ORDER_ISSUED, TIMELINE_ORDER_NOT_ISSUED, TIMELINE_ORDER_NOT_PUNISHED, TIMELINE_ORDER_PUNISHED from db.constants import TIMELINE_ORDER_CONFIRMED, TIMELINE_ORDER_NOT_PUNISHED, TIMELINE_ORDER_PUNISHED
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 domsubusers_doms, order_status_by_id, order_status_put, order_status_confirm, timeline_event_put from db.queries import domsubusers_doms, order_status_by_id, order_status_put, order_status_confirm, timeline_event_put
@ -10,55 +10,57 @@ from mastodon import Mastodon
from telegram.telegram import Telegram from telegram.telegram import Telegram
from settings import ENV from settings import ENV
from util import timezone from util import timezone
from jinja2 import Environment, PackageLoader, select_autoescape
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
template_env = Environment(
loader=PackageLoader("orders"),
autoescape=select_autoescape()
)
template_env.globals['ENV'] = ENV
def filter_short_time(value):
return value.strftime("%I:%M %p")
template_env.filters["short_time"] = filter_short_time
async def order_mastodon_post(session, orders_pool, orders_str, repeats, due_at, verify_at=None): async def order_mastodon_post(session, orders_pool, orders_str, repeats, due_at, verify_at=None):
user = orders_pool.user user = orders_pool.user
template = template_env.get_template('mastodon_order') post = "Here are today's orders for @%s -\n\n" % user.mastodon_account()
post = template.render( post += orders_str + "\n\n"
user=user, if repeats > 1:
orders_str=orders_str, post += f"These are the same orders from the last {repeats} days\n\n"
repeats=repeats, post += "Proof of compliance is due by " + due_at.strftime("%I:%M %p") + "\n"
due_at=due_at, if verify_at is not None:
verify_at=verify_at post += "Verification due by " + verify_at.strftime("%I:%M %p") + "\n"
) post += "\n"
if ENV == 'dev':
post += "⚠️ DEV"
elif user.mastodon_attn_list:
post += f"ATTN - {user.mastodon_attn_list}\n"
m = Mastodon(session) m = Mastodon(session)
return await m.statusPost(post, user) return await m.statusPost(post, user)
async def order_telegram_post(session, orders_pool, orders_str, repeats, due_at, m_url, verify_at=None): async def order_telegram_post(session, orders_pool, orders_str, repeats, due_at, m_url, verify_at=None):
template = template_env.get_template("telegram_order") post = "Here are your orders -\n\n"
post += orders_str + "\n\n"
post = template.render( if repeats > 1:
orders_str = orders_str, post += f"These are the same orders from the last {repeats} days\n\n"
repeats=repeats, post += "Proof of compliance is due by " + due_at.strftime("%I:%M %p") + "\n"
due_at=due_at, if verify_at is not None:
verify_at=verify_at, post += "Verification due by " + verify_at.strftime("%I:%M %p") + "\n"
m_url=m_url post += "\n"
) post += m_url
if ENV == 'dev':
post += "\n⚠️ DEV"
t = Telegram(session) t = Telegram(session)
await t.message_send(orders_pool.user.telegram_chat_id, post) await t.message_send(orders_pool.user.telegram_chat_id, post)
async def order_telegram_message(session, orders_pool, message): async def order_telegram_post_need_mastodon(session, orders_pool):
template = template_env.get_template("telegram_message") post = "Cannot issue an order without a mastodon username"
if ENV == 'dev':
post += "\n⚠️ DEV"
post = template.render( t = Telegram(session)
message=message await t.message_send(orders_pool.user.telegram_chat_id, post)
)
async def order_telegram_post_none(session, orders_pool):
post = "No orders for today"
if ENV == 'dev':
post += "\n⚠️ DEV"
t = Telegram(session) t = Telegram(session)
await t.message_send(orders_pool.user.telegram_chat_id, post) await t.message_send(orders_pool.user.telegram_chat_id, post)
@ -68,33 +70,15 @@ async def order_issue(orders_pool):
user = orders_pool.user user = orders_pool.user
if user.mastodon_username is None: if user.mastodon_username is None:
logger.info(f"{orders_pool} - Cannot issue order without mastodon username") logger.info('Cannot issue order without mastodon username')
await order_telegram_message( await order_telegram_post_need_mastodon(session, orders_pool)
session, orders_pool,
"Cannot issue order without mastodon username"
)
timeline_event_put(
TIMELINE_ORDER_NOT_ISSUED,
"Cannot issue order without mastodon username",
user=orders_pool.user,
orders_pool=orders_pool
)
return { "reason": "Cannot issue order without mastodon username" } return { "reason": "Cannot issue order without mastodon username" }
orders_info = generate_order(orders_pool) orders_info = generate_order(orders_pool)
if 'orders' not in orders_info: if 'orders' not in orders_info:
logger.info(f"{orders_pool} - {orders_info['reason']}") logger.info(f"{orders_pool} - {orders_info['reason']}")
await order_telegram_message( await order_telegram_post_none(session, orders_pool)
session, orders_pool,
"No orders this time"
)
timeline_event_put(
TIMELINE_ORDER_NOT_ISSUED,
orders_info['reason'],
user=orders_pool.user,
orders_pool=orders_pool
)
return { "reason": orders_info['reason'] } return { "reason": orders_info['reason'] }
orders_str = "\n".join(orders_info['orders']) orders_str = "\n".join(orders_info['orders'])
@ -129,7 +113,8 @@ async def order_issue(orders_pool):
verify_at=verify_at verify_at=verify_at
) )
order_status = order_status_put( return {
"order_status" : order_status_put(
orders_pool, orders_pool,
orders_pool.user, orders_pool.user,
m_status['id'], m_status['id'],
@ -137,34 +122,20 @@ async def order_issue(orders_pool):
due_at, due_at,
orders_str, orders_str,
verify_at=verify_at verify_at=verify_at
) ),
timeline_event_put(
TIMELINE_ORDER_ISSUED,
order_status.text.split("\n")[0],
user=orders_pool.user,
orders_pool=orders_pool,
order_status=order_status,
extra={
"mastodon_status_url": m_status['url']
}
)
logger.info(f"Issued {order_status}")
return {
"order_status" : order_status,
"mastodon_status": m_status "mastodon_status": m_status
} }
async def punishment_mastodon_post(session, orders_pool, punishment_str, reply_id=None): async def punishment_mastodon_post(session, orders_pool, punishment_str, reply_id=None):
user = orders_pool.user user = orders_pool.user
template = template_env.get_template('mastodon_punishment') post = "@%s has failed to post proof of compliance. Here is the punishment -\n\n" % user.mastodon_account()
post = template.render( post += punishment_str + "\n\n"
user=user,
punishment_str=punishment_str if ENV == 'dev':
) post += "⚠️ DEV"
elif user.mastodon_attn_list:
post += f"ATTN - {user.mastodon_attn_list}\n"
m = Mastodon(session) m = Mastodon(session)
return await m.statusPost( return await m.statusPost(
@ -174,19 +145,18 @@ async def punishment_mastodon_post(session, orders_pool, punishment_str, reply_i
) )
async def punishment_telegram_post(session, orders_pool, punishment_str, m_url): async def punishment_telegram_post(session, orders_pool, punishment_str, m_url):
template = template_env.get_template('telegram_punishment') post = "You failed to show proof of compliance. Here is your punishment -\n\n"
post += punishment_str + "\n\n"
post = template.render( post += m_url
punishment_str=punishment_str, if ENV == 'dev':
m_url=m_url post += "\n\n⚠️ DEV"
)
t = Telegram(session) t = Telegram(session)
await t.message_send(orders_pool.user.telegram_chat_id, post) await t.message_send(orders_pool.user.telegram_chat_id, post)
async def punishment_issue(session, order_status): async def punishment_issue(session, order_status):
if order_status.pool is None or order_status.pool.punishment_pool is None: 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}, no punishment pool for order pool {order_status.pool.name}') logger.info(f'Unable to issue a punishment for {order_status.id}, no punishment pool for order pool {order_status.pool.name}')
return { "reason": "No punishment pool"} return { "reason": "No punishment pool"}
punishment_pool = order_status.pool.punishment_pool punishment_pool = order_status.pool.punishment_pool
@ -247,7 +217,7 @@ async def order_check(order_status_id):
user = order_status.user user = order_status.user
if order_status.punishment.count() > 0: if order_status.punishment.count() > 0:
logger.info(f'Punishment already issued for {order_status}') logger.info(f'Punishment already issued for {order_status.id}')
return return
m = Mastodon(session) m = Mastodon(session)
@ -297,7 +267,7 @@ async def order_check(order_status_id):
confirmed_at = d['created_at'] confirmed_at = d['created_at']
order_status_confirm(order_status.id, confirmed_at) order_status_confirm(order_status.id, confirmed_at)
logger.info(f"Confirmed order {order_status}") logger.info('Confirmed order %s' % (order_status.id))
timeline_event_put( timeline_event_put(
TIMELINE_ORDER_CONFIRMED, TIMELINE_ORDER_CONFIRMED,
order_status.text.split("\n")[0], order_status.text.split("\n")[0],
@ -311,7 +281,7 @@ async def order_check(order_status_id):
break break
if confirmed_at is None: if confirmed_at is None:
logger.info(f"Order {order_status} remains unconfirmed") logger.info('Order %s remains unconfirmed' % (order_status.id))
due_at = datetime.datetime.fromisoformat(order_status.due_at) due_at = datetime.datetime.fromisoformat(order_status.due_at)
if(due_at < datetime.datetime.now(datetime.UTC)): if(due_at < datetime.datetime.now(datetime.UTC)):
@ -327,7 +297,7 @@ async def order_check(order_status_id):
elif user.verify_mastodon_favorite and had_favorites is False: elif user.verify_mastodon_favorite and had_favorites is False:
reason = "No replies had a favorite from a dom or tagged account" reason = "No replies had a favorite from a dom or tagged account"
logger.info(f"Time to issue a punishment for {order_status}") logger.info('Time to issue a punishment for %s' % order_status.id)
issue_result = await punishment_issue(session, order_status) issue_result = await punishment_issue(session, order_status)

View file

@ -92,11 +92,12 @@ class OrderScheduler():
) )
return return
logger.info(f'Issuing order for {orders_pool.name}[{orders_pool.user.telegram_username}]')
issue_result = await order_issue(orders_pool) issue_result = await order_issue(orders_pool)
if 'order_status' in issue_result: if 'order_status' in issue_result:
order_status = issue_result['order_status'] order_status = issue_result['order_status']
if order_status.due_at is not None or order_status.verify_at is not None: if order_status.due_at is not None or order_status.verify_at is not None:
# Schedule check # Schedule check
check_time = order_status.verify_at if order_status.verify_at is not None else order_status.due_at check_time = order_status.verify_at if order_status.verify_at is not None else order_status.due_at
@ -105,6 +106,23 @@ class OrderScheduler():
self.scheduled_check, self.scheduled_check,
args=(order_status.id,) args=(order_status.id,)
) )
timeline_event_put(
TIMELINE_ORDER_ISSUED,
order_status.text.split("\n")[0],
user=orders_pool.user,
orders_pool=orders_pool,
order_status=order_status,
extra={
"mastodon_status_url": issue_result['mastodon_status']['url']
}
)
elif 'reason' in issue_result:
timeline_event_put(
TIMELINE_ORDER_NOT_ISSUED,
issue_result['reason'],
user=orders_pool.user,
orders_pool=orders_pool
)
async def scheduled_check(self, outstanding_order_id): async def scheduled_check(self, outstanding_order_id):
await order_check(outstanding_order_id) await order_check(outstanding_order_id)

View file

@ -1,9 +0,0 @@
{% block main %}{% endblock %}
{% block attn %}
{% if user.mastodon_attn_list %}
ATTN - {{user.mastodon_attn_list}}
{% endif %}
{% endblock %}
{% block env %}
{% if ENV == 'dev' %}⚠️ DEV{% endif %}
{% endblock %}

View file

@ -1,16 +0,0 @@
{% extends "mastodon_base" %}
{% block main %}
Here are today's orders for @{{user.mastodon_account()}}
{{orders_str}}
{% if repeats > 1 %}
These are the same orders from the last {{repeats}} days.
{% endif %}
Proof of compliance is due by {{due_at|short_time}}
{% if verify_at %}
Verification due by {{verify_at|short_time}}
{% endif %}
{% endblock %}

View file

@ -1,7 +0,0 @@
{% extends "mastodon_base" %}
{% block main %}
@{{user.mastodon_account()}} failed to show proof of compliance. Here is the punishment -
{{punishment_str}}
{% endblock %}

View file

@ -1,2 +0,0 @@
{% block main %}{% endblock %}
{% if ENV == 'dev' %}⚠️ DEV{% endif %}

View file

@ -1,4 +0,0 @@
{% extends "telegram_base" %}
{% block main %}
{{message}}
{% endblock %}

View file

@ -1,15 +0,0 @@
{% extends "telegram_base" %}
{% block main %}
Here are your orders -
{{orders_str}}
{% if repeats > 1 %}
These are the same orders from the last {{repeats}} days.
{% endif %}
Proof of compliance is due by {{due_at|short_time}}
{% if verify_at %}
Proof of verification is due by {{verify_at|short_time}}
{% endif %}
{{m_url}}
{% endblock %}

View file

@ -1,9 +0,0 @@
{% extends "telegram_base" %}
{% block main %}
You failed to show proof of compliance. Here is your punishment -
{{punishment_str}}
{{m_url}}
{% endblock %}

View file

@ -6,7 +6,7 @@ from flask import Blueprint, jsonify, abort, request
from flask_login import login_required, current_user from flask_login import login_required, current_user
from db.constants import TIMELINE_ORDERS_POOL_CREATED, TIMELINE_ORDERS_POOL_DELETED, TIMELINE_ORDERS_POOL_UPDATED from db.constants import TIMELINE_ORDERS_POOL_CREATED, TIMELINE_ORDERS_POOL_DELETED, TIMELINE_ORDERS_POOL_UPDATED
from db.models import database, OrdersPool, Order, OrderAddOn, MastodonServer from db.models import database, OrdersPool, Order, OrderAddOn, MastodonServer
from db.queries import timeline_event_put, timeline_event_recent, user_can_orders_pools_details, user_can_orders_pools_edit, user_can_orders_pools_view, user_doms, user_get, domsubusers_list, orders_pool_list, orders_pool, mastodon_server_get, mastodon_server_put, user_has_doms, user_mastodon_server_set, user_preferences_set from db.queries import timeline_event_put, timeline_event_recent, user_get, domsubusers_list, orders_pool_list, orders_pool, mastodon_server_get, mastodon_server_put, user_has_doms, user_mastodon_server_set, user_preferences_set
from settings import MASTODON_OAUTH_CLIENT_NAME, MASTODON_OAUTH_REDIRECT_URI, MASTODON_OAUTH_SCOPES, MASTODON_OAUTH_CLIENT_WEBSITE from settings import MASTODON_OAUTH_CLIENT_NAME, MASTODON_OAUTH_REDIRECT_URI, MASTODON_OAUTH_SCOPES, MASTODON_OAUTH_CLIENT_WEBSITE
from util import time_sqlite from util import time_sqlite
@ -79,14 +79,13 @@ def timeline():
"username": t.user.telegram_username, "username": t.user.telegram_username,
"actor_username": t.actor.telegram_username if t.actor is not None else None, "actor_username": t.actor.telegram_username if t.actor is not None else None,
"orders_pool": { "orders_pool": {
"id": t.orders_pool.id if user_can_orders_pools_edit(current_user.db_user, t.user) else None, "id": t.orders_pool.id,
"name": t.orders_pool.name, "name": t.orders_pool.name,
} if ( } if t.orders_pool is not None else None,
t.orders_pool is not None and
user_can_orders_pools_view(current_user.db_user, t.user)
) else None,
"order_status": t.order_status.text if t.order_status is not None else None, "order_status": t.order_status.text if t.order_status is not None else None,
} for t in timeline_event_recent(current_user.db_user.id) } for t in timeline_event_recent(
subs,
[current_user.db_user.id,])
]) ])
@api.route('/mastodon_oauth') @api.route('/mastodon_oauth')
@ -164,28 +163,16 @@ def authorized_sub(func):
@login_required @login_required
@authorized_sub @authorized_sub
def sub(username, sub): def sub(username, sub):
if request.method == "POST": if user_has_doms(sub) and sub.id == current_user.db_user.id:
if (user_has_doms(sub) and sub.id == current_user.db_user.id and
('permission_orders_pools_view' in request.json or
'permission_orders_pools_details' in request.json or
'permission_orders_pools_edit' in request.json)):
abort(403) abort(403)
return return
if 'verify_mastodon_favorite' in request.json: if request.method == "POST":
sub.verify_mastodon_favorite = bool(request.json['verify_mastodon_favorite']) sub.verify_mastodon_favorite = bool(request.json['verify_mastodon_favorite'])
if 'verify_mastodon_alt_text' in request.json:
sub.verify_mastodon_alt_text = bool(request.json['verify_mastodon_alt_text']) sub.verify_mastodon_alt_text = bool(request.json['verify_mastodon_alt_text'])
if 'verify_delay' in request.json and request.json['verify_delay'] is not None: if request.json['verify_delay'] is not None:
sub.verify_delay = int(request.json["verify_delay"]) sub.verify_delay = int(request.json["verify_delay"])
if 'permission_orders_pools_view' in request.json:
sub.permission_orders_pools_view = bool(request.json['permission_orders_pools_view'])
if 'permission_orders_pools_details' in request.json:
sub.permission_orders_pools_details = bool(request.json['permission_orders_pools_details'])
if 'permission_orders_pools_edit' in request.json:
sub.permission_orders_pools_edit = bool(request.json['permission_orders_pools_edit'])
sub.save() sub.save()
return ('', 204) return ('', 204)
@ -196,27 +183,13 @@ def sub(username, sub):
"mastodon_username": sub.mastodon_username, "mastodon_username": sub.mastodon_username,
"verify_mastodon_alt_text": sub.verify_mastodon_alt_text, "verify_mastodon_alt_text": sub.verify_mastodon_alt_text,
"verify_mastodon_favorite": sub.verify_mastodon_favorite, "verify_mastodon_favorite": sub.verify_mastodon_favorite,
"verify_delay": sub.verify_delay, "verify_delay": sub.verify_delay
"can_edit_permissions": current_user.db_user in user_doms(sub.id),
"permission_orders_pools_view": sub.permission_orders_pools_view,
"permission_orders_pools_details": sub.permission_orders_pools_details,
"permission_orders_pools_edit": sub.permission_orders_pools_edit
}) })
@api.route('/orders/') @api.route('/orders/')
@login_required @login_required
def my_order_sets(): def my_order_sets():
user = current_user.db_user return jsonify([
result = {
'permissions': {
'can_view': user_can_orders_pools_view(user, user),
'can_details': user_can_orders_pools_details(user, user),
'can_edit': user_can_orders_pools_edit(user, user)
}
}
if result['permissions']['can_details']:
result['pools'] = [
{ {
'id': op.id, 'id': op.id,
'name': op.name, 'name': op.name,
@ -234,39 +207,13 @@ def my_order_sets():
} }
for op for op
in orders_pool_list(current_user.db_user) in orders_pool_list(current_user.db_user)
] ])
elif result['permissions']['can_view']:
result['pools'] = [
{
'id': op.id,
'name': op.name,
'scheduled': op.scheduled,
'time': op.time,
'weekends': op.weekends,
'weekdays': op.weekdays,
'probability': op.probability,
'punishment_pool_name': op.punishment_pool.name if op.punishment_pool is not None else None,
}
for op
in orders_pool_list(current_user.db_user)
]
return jsonify(result)
@api.route('/orders/<username>/sets') @api.route('/orders/<username>/sets')
@login_required @login_required
@authorized_sub @authorized_sub
def sub_order_sets(username, sub): def sub_order_sets(username, sub):
result = { return jsonify([
'permissions': {
'can_view': user_can_orders_pools_view(current_user.db_user, sub),
'can_details': user_can_orders_pools_details(current_user.db_user, sub),
'can_edit': user_can_orders_pools_edit(current_user.db_user, sub)
}
}
if result['permissions']['can_details']:
result['pools'] = [
{ {
'id': op.id, 'id': op.id,
'name': op.name, 'name': op.name,
@ -284,33 +231,12 @@ def sub_order_sets(username, sub):
} }
for op for op
in orders_pool_list(sub.id) in orders_pool_list(sub.id)
] ])
elif result['permissions']['can_view']:
result['pools'] = [
{
'id': op.id,
'name': op.name,
'scheduled': op.scheduled,
'time': op.time,
'weekends': op.weekends,
'weekdays': op.weekdays,
'probability': op.probability,
'punishment_pool_name': op.punishment_pool.name if op.punishment_pool is not None else None,
}
for op
in orders_pool_list(sub.id)
]
return jsonify(result)
@api.route('/orders/<username>/sets/', methods=['POST']) @api.route('/orders/<username>/sets/', methods=['POST'])
@login_required @login_required
@authorized_sub @authorized_sub
def sub_order_set_create(username, sub): def sub_order_set_create(username, sub):
if not user_can_orders_pools_edit(current_user.db_user, sub):
abort(403)
return
# Create new # Create new
with database.atomic() as transaction: with database.atomic() as transaction:
try: try:
@ -361,10 +287,6 @@ def sub_order_set(username, set_id, sub):
op = orders_pool(sub.id, set_id) op = orders_pool(sub.id, set_id)
if request.method == 'POST': if request.method == 'POST':
if not user_can_orders_pools_edit(current_user.db_user, sub):
abort(403)
return
def update_add_ons(order, add_ons): def update_add_ons(order, add_ons):
for updated_add_on in add_ons: for updated_add_on in add_ons:
if isinstance(updated_add_on['id'], int): if isinstance(updated_add_on['id'], int):
@ -449,10 +371,6 @@ def sub_order_set(username, set_id, sub):
abort(500) abort(500)
elif request.method == 'DELETE': elif request.method == 'DELETE':
try: try:
if not user_can_orders_pools_edit(current_user.db_user, sub):
abort(403)
return
op.delete_instance(recursive=True) op.delete_instance(recursive=True)
timeline_event_put( timeline_event_put(

View file

@ -81,11 +81,12 @@ def index():
print('Redirect to dashboard') print('Redirect to dashboard')
return redirect('/dashboard/') return redirect('/dashboard/')
@app.route('/dashboard/') @app.route('/profile/', defaults={'path': ''})
@app.route('/profile/') @app.route('/dashboard/', defaults={'path': ''})
@app.route('/orders/<path:path>') @app.route('/orders/<path:path>')
@app.route('/dashboard/<path:path>')
@login_required @login_required
def dashboard(path=''): def dashboard(path):
return render_template('dashboard.html') return render_template('dashboard.html')
@app.route('/logout') @app.route('/logout')

View file

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { Button, Modal, Text, Flex, ButtonVariant } from "@mantine/core"; import { Button, Modal, Text, Flex } from "@mantine/core";
export const ConfirmDialogButton: React.FC<{ export const ConfirmDialogButton: React.FC<{
text: string; text: string;
@ -9,15 +9,7 @@ export const ConfirmDialogButton: React.FC<{
buttonColor: string; buttonColor: string;
onConfirm: () => void; onConfirm: () => void;
children: React.ReactNode; children: React.ReactNode;
variant?: ButtonVariant; }> = ({ text, buttonText, buttonColor, onConfirm, children }) => {
}> = ({
text,
buttonText,
buttonColor,
onConfirm,
children,
variant = "filled",
}) => {
const [opened, { open, close }] = useDisclosure(); const [opened, { open, close }] = useDisclosure();
const handleConfirm = React.useCallback(() => { const handleConfirm = React.useCallback(() => {
@ -30,15 +22,13 @@ export const ConfirmDialogButton: React.FC<{
<Modal opened={opened} onClose={close} p="sm" withCloseButton={false}> <Modal opened={opened} onClose={close} p="sm" withCloseButton={false}>
<Text mb="xl">{text}</Text> <Text mb="xl">{text}</Text>
<Flex gap="md" justify="flex-end"> <Flex gap="md" justify="flex-end">
<Button variant="outline" onClick={close}> <Button onClick={close}>Cancel</Button>
Cancel
</Button>
<Button color={buttonColor} onClick={handleConfirm}> <Button color={buttonColor} onClick={handleConfirm}>
{buttonText} {buttonText}
</Button> </Button>
</Flex> </Flex>
</Modal> </Modal>
<Button variant={variant} color={buttonColor} onClick={open}> <Button color={buttonColor} onClick={open}>
{children} {children}
</Button> </Button>
</> </>

View file

@ -3,7 +3,7 @@ import { Text, Title, Flex, Card, Image } from "@mantine/core";
import { useLoaderData } from "react-router"; import { useLoaderData } from "react-router";
import { IconPencil } from "@tabler/icons-react"; import { IconPencil } from "@tabler/icons-react";
import { NavigateButton } from "./NavigateButton"; import { NavigateButton } from "./NavigateButton";
import { OrderSets, OrderSetsResponse } from "./OrderSets"; import { OrderSetProps, OrderSets } from "./OrderSets";
import { useUserContext } from "./UserContext"; import { useUserContext } from "./UserContext";
import { TimelineList } from "./TimelineList"; import { TimelineList } from "./TimelineList";
@ -25,7 +25,15 @@ const SubsList: React.FC<SubsListProps> = ({ subs }) => (
</Title> </Title>
<Flex gap="md" wrap="wrap"> <Flex gap="md" wrap="wrap">
{subs.map(({ sub_username, telegram_photo_url }) => ( {subs.map(({ sub_username, telegram_photo_url }) => (
<Card key={sub_username} padding="lg" withBorder w="320px"> <Card
key={sub_username}
shadow="sm"
padding="lg"
radius="md"
withBorder
bg="gray.2"
w="320px"
>
{telegram_photo_url ? ( {telegram_photo_url ? (
<Card.Section> <Card.Section>
<Image <Image
@ -54,20 +62,14 @@ const SubsList: React.FC<SubsListProps> = ({ subs }) => (
export const Dashboard: React.FC = () => { export const Dashboard: React.FC = () => {
const [orderSets, subs, timeline] = const [orderSets, subs, timeline] =
useLoaderData< useLoaderData<
[OrderSetsResponse, SubsListProps["subs"], TimelineEvent[]] [OrderSetProps["orderSets"], SubsListProps["subs"], TimelineEvent[]]
>(); >();
const { username } = useUserContext(); const { username } = useUserContext();
return ( return (
<> <>
{timeline.length > 0 ? <TimelineList timeline={timeline} /> : null} {timeline.length > 0 ? <TimelineList timeline={timeline} /> : null}
{orderSets.permissions?.can_view && orderSets.pools.length > 0 ? ( <OrderSets orderSets={orderSets} username={username} />
<OrderSets
orderSets={orderSets.pools}
permissions={orderSets.permissions}
username={username}
/>
) : null}
{subs.length > 0 ? <SubsList subs={subs} /> : null} {subs.length > 0 ? <SubsList subs={subs} /> : null}
</> </>
); );

View file

@ -1,32 +1,25 @@
import { import { Avatar, Text, Container, Flex, UnstyledButton } from "@mantine/core";
Avatar,
Text,
Container,
Flex,
UnstyledButton,
Anchor,
} from "@mantine/core";
import React from "react"; import React from "react";
import { useUserContext } from "./UserContext"; import { useUserContext } from "./UserContext";
import { Link, Outlet, useNavigate } from "react-router"; import { Outlet, useNavigate } from "react-router";
export const Header: React.FC = () => { export const Header: React.FC = () => {
const { username, telegram_photo_url } = useUserContext(); const { username, telegram_photo_url } = useUserContext();
const navigate = useNavigate(); const navigate = useNavigate();
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
navigate("/profile/"); navigate("/profile");
}, []); }, []);
return ( return (
<Container p="sm"> <Container p="sm">
<Flex justify="flex-end"> <Flex justify="flex-end">
<Anchor component={Link} to={"/profile/"}> <UnstyledButton onClick={handleClick}>
<Flex align="center" gap="sm"> <Flex align="center" gap="sm">
<Text>{username}</Text> <Text c="blue">{username}</Text>
<Avatar src={telegram_photo_url} /> <Avatar src={telegram_photo_url} />
</Flex> </Flex>
</Anchor> </UnstyledButton>
</Flex> </Flex>
<Outlet /> <Outlet />
</Container> </Container>

View file

@ -16,7 +16,6 @@ import {
NumberInput, NumberInput,
Text, Text,
Select, Select,
Anchor,
} 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";
@ -83,22 +82,9 @@ export const OrderSet: React.FC = () => {
const [orderSets, setOrderSets] = React.useState([]); const [orderSets, setOrderSets] = React.useState([]);
React.useEffect(() => { React.useEffect(() => {
fetch(`/api/orders/${username}/sets`).then(async (response) => { fetch(`/api/orders/${username}/sets`)
if (response.ok) { .then((response) => response.json())
const data = await response.json(); .then(setOrderSets);
if (!data.permissions?.can_edit) {
notifications.show({
title: "Error",
message: "Not authorized",
color: "red",
});
navigate(`/dashboard/`);
}
setOrderSets(data.pools);
}
});
}, [username]); }, [username]);
const [showScheduling, setShowScheduling] = React.useState( const [showScheduling, setShowScheduling] = React.useState(
@ -262,9 +248,7 @@ export const OrderSet: React.FC = () => {
<> <>
<Box mb="lg"> <Box mb="lg">
<Title order={1}>{orderSet?.name || "New Order Set"}</Title> <Title order={1}>{orderSet?.name || "New Order Set"}</Title>
<Anchor component={Link} to={`/orders/${username}`}> <Link to={`/orders/${username}`}>Return to {username}</Link>
Return to {username}
</Anchor>
</Box> </Box>
<form id="order-set" onSubmit={handleSubmit}> <form id="order-set" onSubmit={handleSubmit}>
<TextInput {...form.getInputProps("name")} label="Name" /> <TextInput {...form.getInputProps("name")} label="Name" />
@ -275,7 +259,7 @@ export const OrderSet: React.FC = () => {
mt="lg" mt="lg"
/> />
<Collapse in={showScheduling}> <Collapse in={showScheduling}>
<Paper> <Paper bg="gray.2">
<Input.Wrapper <Input.Wrapper
error={form.getInputProps("probability").error} error={form.getInputProps("probability").error}
label="Probability" label="Probability"
@ -332,7 +316,7 @@ export const OrderSet: React.FC = () => {
</Title> </Title>
{form.getValues().orders.map(({ id: order_id, _delete }, idx) => {form.getValues().orders.map(({ id: order_id, _delete }, idx) =>
_delete ? null : ( _delete ? null : (
<Paper key={order_id}> <Paper key={order_id} bg="gray.2">
<Flex gap="xl" justify="space-between"> <Flex gap="xl" justify="space-between">
<TextInput <TextInput
{...form.getInputProps(`orders.${idx}.name`)} {...form.getInputProps(`orders.${idx}.name`)}
@ -422,7 +406,7 @@ export const OrderSet: React.FC = () => {
</Button> </Button>
<Box my="40px" /> <Box my="40px" />
<Affix position={{ bottom: 0 }} w="100%"> <Affix position={{ bottom: 0 }} w="100%">
<Paper mb={0}> <Paper mb={0} bg="gray.1">
<Container> <Container>
<Flex justify="flex-end"> <Flex justify="flex-end">
<Button type="submit" form="order-set"> <Button type="submit" form="order-set">

View file

@ -9,7 +9,6 @@ import {
RingProgress, RingProgress,
Alert, Alert,
Grid, Grid,
Anchor,
} from "@mantine/core"; } from "@mantine/core";
import { IconAlertTriangle } from "@tabler/icons-react"; import { IconAlertTriangle } from "@tabler/icons-react";
import { TimeValue } from "@mantine/dates"; import { TimeValue } from "@mantine/dates";
@ -22,38 +21,20 @@ import { useUserContext } from "./UserContext";
import { DonutChart } from "@mantine/charts"; import { DonutChart } from "@mantine/charts";
const COLORS_ROTATION = [ const COLORS_ROTATION = [
"orange.8", "teal",
"gray.7", "pink",
"orange.7", "lime",
"gray.6", "violet",
"orange.6", "orange",
"gray.5", "blue",
"orange.5", "yellow",
"gray.4", "grape",
"green",
"red",
"cyan",
"gray",
]; ];
export interface OrderSetsResponse {
pools: (Pick<
OrderSet,
| "id"
| "name"
| "scheduled"
| "time"
| "weekends"
| "weekdays"
| "orders"
| "probability"
> & {
orders: Pick<OrderSetOrder, "id" | "name" | "weight">;
punishment_pool_name: string;
})[];
permissions: {
can_view: boolean;
can_details: boolean;
can_edit: boolean;
};
}
export interface OrderSetProps { export interface OrderSetProps {
orderSets: (Pick< orderSets: (Pick<
OrderSet, OrderSet,
@ -71,14 +52,12 @@ export interface OrderSetProps {
})[]; })[];
username: string; username: string;
linkBack?: React.ReactNode; linkBack?: React.ReactNode;
permissions?: OrderSetsResponse["permissions"];
} }
export const OrderSets: React.FC<OrderSetProps> = ({ export const OrderSets: React.FC<OrderSetProps> = ({
orderSets, orderSets,
username, username,
linkBack, linkBack,
permissions,
}) => { }) => {
const { username: current_user } = useUserContext(); const { username: current_user } = useUserContext();
const fetcher = useFetcher(); const fetcher = useFetcher();
@ -95,13 +74,12 @@ export const OrderSets: React.FC<OrderSetProps> = ({
const [isMastodonSet, setIsMastodonSet] = React.useState(true); const [isMastodonSet, setIsMastodonSet] = React.useState(true);
React.useEffect(() => { React.useEffect(() => {
if (username) { if (username) {
fetch(`/api/subs/${username}`).then(async (response) => { fetch(`/api/subs/${username}`)
if (response.ok) { .then((response) => response.json())
const data = await response.json(); .then((data) => {
if (!data.mastodon_server || !data.mastodon_username) { if (!data.mastodon_server || !data.mastodon_username) {
setIsMastodonSet(false); setIsMastodonSet(false);
} }
}
}); });
} }
}, [username]); }, [username]);
@ -109,12 +87,12 @@ export const OrderSets: React.FC<OrderSetProps> = ({
const [portalRef, setPortalRef] = React.useState<HTMLElement | null>(); const [portalRef, setPortalRef] = React.useState<HTMLElement | null>();
return ( return (
<Box my="lg"> <>
<Box mb="lg"> <Box mb="lg">
<Title order={1}>Order Sets for {username}</Title> <Title order={1}>Order Sets for {username}</Title>
{linkBack ? linkBack : null} {linkBack ? linkBack : null}
</Box> </Box>
{isMastodonSet ? null : ( {orderSets.length > 0 && isMastodonSet ? null : (
<Flex justify="center"> <Flex justify="center">
<Alert <Alert
variant="light" variant="light"
@ -129,9 +107,7 @@ export const OrderSets: React.FC<OrderSetProps> = ({
{username === current_user ? ( {username === current_user ? (
<> <>
<br /> <br />
<Anchor component={Link} to="/profile/"> <Link to="/profile/">Edit Profile</Link>
Edit Profile
</Anchor>
</> </>
) : null} ) : null}
</Alert> </Alert>
@ -144,7 +120,6 @@ export const OrderSets: React.FC<OrderSetProps> = ({
} }
}} }}
> >
{permissions?.can_view ? (
<Grid gutter="md"> <Grid gutter="md">
{orderSets {orderSets
? orderSets.map( ? orderSets.map(
@ -165,6 +140,7 @@ export const OrderSets: React.FC<OrderSetProps> = ({
padding="lg" padding="lg"
radius="md" radius="md"
withBorder withBorder
bg="gray.2"
mb="0" mb="0"
> >
<Flex direction="column" gap="md" h="100%"> <Flex direction="column" gap="md" h="100%">
@ -187,10 +163,10 @@ export const OrderSets: React.FC<OrderSetProps> = ({
</Flex> </Flex>
<Flex gap="md" align="center"> <Flex gap="md" align="center">
{weekdays ? ( {weekdays ? (
<Badge color="orange.7">Weekdays</Badge> <Badge color="blue">Weekdays</Badge>
) : null} ) : null}
{weekends ? ( {weekends ? (
<Badge color="orange.7">Weekends</Badge> <Badge color="blue">Weekends</Badge>
) : null} ) : null}
<RingProgress <RingProgress
size={30} size={30}
@ -201,10 +177,7 @@ export const OrderSets: React.FC<OrderSetProps> = ({
</Text> </Text>
} }
sections={[ sections={[
{ { color: "cyan", value: probability * 100 },
color: "orange",
value: probability * 100,
},
]} ]}
/> />
</Flex> </Flex>
@ -215,8 +188,8 @@ export const OrderSets: React.FC<OrderSetProps> = ({
<b>Punishments:</b> {punishment_pool_name} <b>Punishments:</b> {punishment_pool_name}
</Text> </Text>
) : null} ) : null}
<Flex justify="end" align="flex-end" gap="sm"> <Flex justify="end" align="flex-end" gap="md">
{permissions?.can_details && orders.length > 0 ? ( {orders.length > 0 ? (
<DonutChart <DonutChart
flex={1} flex={1}
size={130} size={130}
@ -225,34 +198,23 @@ export const OrderSets: React.FC<OrderSetProps> = ({
name, name,
value: weight, value: weight,
color: color:
COLORS_ROTATION[ COLORS_ROTATION[idx % COLORS_ROTATION.length],
idx % COLORS_ROTATION.length
],
}))} }))}
tooltipDataSource="segment" tooltipDataSource="segment"
/> />
) : null} ) : null}
{permissions?.can_edit ? (
<>
<ConfirmDialogButton <ConfirmDialogButton
buttonColor="red.8" buttonColor="red.8"
buttonText="Delete" buttonText="Delete"
variant="transparent"
text={`Are you sure you want to delete ${name}?`} text={`Are you sure you want to delete ${name}?`}
onConfirm={() => handleDelete(id)} onConfirm={() => handleDelete(id)}
> >
<IconTrash /> <IconTrash />
</ConfirmDialogButton> </ConfirmDialogButton>
<NavigateButton <NavigateButton to={`/orders/${username}/${id}`}>
to={`/orders/${username}/${id}`} <IconPencil style={{ marginRight: "0.5rem" }} />
>
<IconPencil
style={{ marginRight: "0.5rem" }}
/>
Edit Edit
</NavigateButton> </NavigateButton>
</>
) : null}
</Flex> </Flex>
</Flex> </Flex>
</Card> </Card>
@ -261,29 +223,13 @@ export const OrderSets: React.FC<OrderSetProps> = ({
) )
: null} : null}
</Grid> </Grid>
) : (
<Flex justify="center">
<Alert
variant="light"
color="orange"
title="Warning"
icon={<IconAlertTriangle />}
my="md"
w="40rem"
>
You are not authorized to view order pools for <b>{username}</b>.
</Alert>
</Flex>
)}
</div> </div>
{permissions?.can_edit ? (
<Box my="lg"> <Box my="lg">
<NavigateButton to={`/orders/${username}/new`}> <NavigateButton to={`/orders/${username}/new`}>
<IconPlus style={{ marginRight: "0.5rem" }} /> <IconPlus style={{ marginRight: "0.5rem" }} />
New New
</NavigateButton> </NavigateButton>
</Box> </Box>
) : null} </>
</Box>
); );
}; };

View file

@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import { useUserContext } from "./UserContext"; import { useUserContext } from "./UserContext";
import { import {
Anchor,
Avatar, Avatar,
Box, Box,
Button, Button,
@ -135,11 +134,9 @@ export const Profile: React.FC = () => {
<Title>{username}</Title> <Title>{username}</Title>
</Flex> </Flex>
<Box mb="md"> <Box mb="md">
<Anchor component={Link} to={`/dashboard/`}> <Link to={`/dashboard`}>Return to dashboard</Link>
Return to dashboard
</Anchor>
</Box> </Box>
<Paper withBorder> <Paper bg="gray.1">
<Title order={4}>Mastodon</Title> <Title order={4}>Mastodon</Title>
<TextInput label="Account" w="50%" value={mastodon_account} /> <TextInput label="Account" w="50%" value={mastodon_account} />
<Button onClick={open}>Authorize with Mastodon</Button> <Button onClick={open}>Authorize with Mastodon</Button>

View file

@ -1,129 +0,0 @@
import React from "react";
import { Button, Checkbox, Paper, Title } from "@mantine/core";
import { useForm } from "@mantine/form";
import { notifications } from "@mantine/notifications";
import { fetchHeaders } from "./fetch";
type ProfilePermissionsProps = Pick<
UserProfile,
| "permission_orders_pools_view"
| "permission_orders_pools_details"
| "permission_orders_pools_edit"
> & { username: string };
type ProfilePermissionsForm = Pick<
UserProfile,
| "permission_orders_pools_view"
| "permission_orders_pools_details"
| "permission_orders_pools_edit"
>;
export const ProfilePermissions: React.FC<ProfilePermissionsProps> = ({
username,
permission_orders_pools_view,
permission_orders_pools_details,
permission_orders_pools_edit,
}) => {
const [loading, setLoading] = React.useState(false);
const [detailsDisabled, setDetailsDisabled] = React.useState(
!permission_orders_pools_view,
);
const [editDisabled, setEditDisabled] = React.useState(
!permission_orders_pools_details,
);
const form = useForm<ProfilePermissionsForm>({
mode: "uncontrolled",
initialValues: {
permission_orders_pools_view,
permission_orders_pools_details,
permission_orders_pools_edit,
},
});
form.watch("permission_orders_pools_view", ({ value }) => {
if (!value) {
form.setFieldValue("permission_orders_pools_details", false);
setDetailsDisabled(true);
} else {
setDetailsDisabled(false);
}
});
form.watch("permission_orders_pools_details", ({ value }) => {
if (!value) {
form.setFieldValue("permission_orders_pools_edit", false);
setEditDisabled(true);
} else {
setEditDisabled(false);
}
});
const handleSubmit = React.useCallback(
form.onSubmit((values) => {
setLoading(true);
fetch(`/api/subs/${username}`, {
method: "POST",
headers: fetchHeaders(),
body: JSON.stringify(values),
})
.then((response) => {
if (response.ok) {
notifications.show({
title: "Success",
message: "Permissions have been saved",
color: "green",
});
} else {
notifications.show({
title: "Error",
message: "There was a problem saving permissions",
color: "red",
});
}
})
.finally(() => {
setLoading(false);
});
}),
[],
);
return (
<Paper>
<form onSubmit={handleSubmit}>
<Title order={4} mb="md">
Permissions
</Title>
<Checkbox
{...form.getInputProps("permission_orders_pools_view", {
type: "checkbox",
})}
key={form.key("permission_orders_pools_view")}
label="Sub can view orders sets"
mb="md"
/>
<Checkbox
{...form.getInputProps("permission_orders_pools_details", {
type: "checkbox",
})}
key={form.key("permission_orders_pools_details")}
disabled={detailsDisabled}
label="Sub can view orders sets details"
mb="md"
/>
<Checkbox
{...form.getInputProps("permission_orders_pools_edit", {
type: "checkbox",
})}
key={form.key("permission_orders_pools_edit")}
disabled={editDisabled}
label="Sub can edit orders sets"
mb="md"
/>
<Button type="submit" loading={loading} mt="md">
Save
</Button>
</form>
</Paper>
);
};

View file

@ -75,7 +75,7 @@ export const ProfileVerification: React.FC<ProfileVerificationProps> = ({
); );
return ( return (
<Paper> <Paper bg="gray.1">
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<Title order={4} mb="md"> <Title order={4} mb="md">
Order Verification Order Verification

View file

@ -0,0 +1,46 @@
import React from "react";
import { Params, useLoaderData, useParams, Link } from "react-router";
import { OrderSetProps, OrderSets } from "./OrderSets";
import { ProfileVerification } from "./ProfileVerification";
import { Title } from "@mantine/core";
export const subOrderSetsLoader = async ({
params: { username },
}: {
params: Params<string>;
}) => fetch(`/api/orders/${username}/sets`).then((response) => response.json());
export const SubOrderSets: React.FC = () => {
const { username: sub_username } = useParams();
const orderSets = useLoaderData<OrderSetProps["orderSets"]>();
const [profile, setProfile] = React.useState<UserProfile | null>(null);
React.useEffect(() => {
fetch(`/api/subs/${sub_username}`)
.then((response) => response.json())
.then(setProfile);
}, [sub_username]);
return (
<>
<OrderSets
username={sub_username}
orderSets={orderSets}
linkBack={<Link to={`/dashboard/`}>Return to dashboard</Link>}
/>
{profile ? (
<>
<Title order={1} mb="md">
Sub Profile
</Title>
<ProfileVerification
username={sub_username}
verify_mastodon_alt_text={profile.verify_mastodon_alt_text}
verify_mastodon_favorite={profile.verify_mastodon_favorite}
verify_delay={profile.verify_delay}
/>
</>
) : null}
</>
);
};

View file

@ -1,4 +1,4 @@
import { Timeline, Text, Title, Box, Flex, Card, Anchor } from "@mantine/core"; import { Timeline, Text, Title, Box, Flex } from "@mantine/core";
import React from "react"; import React from "react";
import moment from "moment"; import moment from "moment";
import { Link } from "react-router"; import { Link } from "react-router";
@ -14,7 +14,6 @@ import {
IconLock, IconLock,
IconX, IconX,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useUserContext } from "./UserContext";
const TIMELINE_TYPE = { const TIMELINE_TYPE = {
ORDER_NOT_ISSUED: { ORDER_NOT_ISSUED: {
@ -29,27 +28,27 @@ const TIMELINE_TYPE = {
}, },
ORDER_CONFIRMED: { ORDER_CONFIRMED: {
title: "Order confirmed", title: "Order confirmed",
color: "green.9", color: "green.6",
bullet: <IconCheck />, bullet: <IconCheck />,
}, },
ORDER_NOT_PUNISHED: { ORDER_NOT_PUNISHED: {
title: "Order not punished", title: "Order not punished",
color: "gray.8", color: "gray.6",
bullet: null, bullet: null,
}, },
ORDER_PUNISHED: { ORDER_PUNISHED: {
title: "Order punished", title: "Order punished",
color: "red.8", color: "red.6",
bullet: <IconAlertCircle />, bullet: <IconAlertCircle />,
}, },
ORDERS_POOL_CREATED: { ORDERS_POOL_CREATED: {
title: "Orders pool created", title: "Orders pool created",
color: "green.6", color: "green.3",
bullet: <IconFilePlus />, bullet: <IconFilePlus />,
}, },
ORDERS_POOL_UPDATED: { ORDERS_POOL_UPDATED: {
title: "Orders pool updated", title: "Orders pool updated",
color: "green.6", color: "green.3",
bullet: <IconFileDiff />, bullet: <IconFileDiff />,
}, },
ORDERS_POOL_DELETED: { ORDERS_POOL_DELETED: {
@ -61,15 +60,11 @@ const TIMELINE_TYPE = {
export const TimelineList: React.FC<{ export const TimelineList: React.FC<{
timeline: TimelineEvent[]; timeline: TimelineEvent[];
}> = ({ timeline }) => { }> = ({ timeline }) => (
const { username: my_username } = useUserContext();
return (
<Box> <Box>
<Title order={1} mb="lg"> <Title order={1} mb="lg">
Timeline Timeline
</Title> </Title>
<Card py="xs">
<Timeline bulletSize={24} lineWidth={2} my="lg"> <Timeline bulletSize={24} lineWidth={2} my="lg">
{timeline.map( {timeline.map(
({ ({
@ -94,23 +89,16 @@ export const TimelineList: React.FC<{
<Flex mt={4} gap="xs"> <Flex mt={4} gap="xs">
{extra?.mastodon_status_url ? ( {extra?.mastodon_status_url ? (
<Text size="xs"> <Text size="xs">
<Anchor href={extra.mastodon_status_url} target="_blank"> <a href={extra.mastodon_status_url} target="_blank">
Mastodon Post <IconExternalLink size="0.75rem" /> Mastodon Post <IconExternalLink size="0.75rem" />
</Anchor> </a>
</Text> </Text>
) : null} ) : null}
{orders_pool ? ( {orders_pool ? (
<Text size="xs"> <Text size="xs">
{orders_pool.id ? ( <Link to={`/orders/${username}/${orders_pool.id}`}>
<Anchor
component={Link}
to={`/orders/${username}/${orders_pool.id}`}
>
{orders_pool.name} {orders_pool.name}
</Anchor> </Link>
) : (
orders_pool.name
)}
</Text> </Text>
) : null} ) : null}
{actor_username ? ( {actor_username ? (
@ -119,15 +107,7 @@ export const TimelineList: React.FC<{
</Text> </Text>
) : null} ) : null}
<Text size="xs"> <Text size="xs">
{username != my_username ? (
<Anchor component={Link} to={`/orders/${username}`}>
<IconLock size="0.75rem" /> {username} <IconLock size="0.75rem" /> {username}
</Anchor>
) : (
<>
<IconLock size="0.75rem" /> {username}
</>
)}
</Text> </Text>
<Text size="xs">{moment(updated_at).fromNow()}</Text> <Text size="xs">{moment(updated_at).fromNow()}</Text>
</Flex> </Flex>
@ -135,7 +115,5 @@ export const TimelineList: React.FC<{
), ),
)} )}
</Timeline> </Timeline>
</Card>
</Box> </Box>
); );
};

View file

@ -1,68 +0,0 @@
import React from "react";
import { Params, useLoaderData, useParams, Link } from "react-router";
import { OrderSetProps, OrderSets, OrderSetsResponse } from "./OrderSets";
import { ProfileVerification } from "./ProfileVerification";
import { Anchor, Title } from "@mantine/core";
import { ProfilePermissions } from "./ProfilePermissions";
export const userOrderSetsLoader = async ({
params: { username },
}: {
params: Params<string>;
}) => fetch(`/api/orders/${username}/sets`).then((response) => response.json());
export const UserOrderSets: React.FC = () => {
const { username: sub_username } = useParams();
const orderSets = useLoaderData<OrderSetsResponse>();
const [profile, setProfile] = React.useState<UserProfile | null>(null);
React.useEffect(() => {
fetch(`/api/subs/${sub_username}`).then(async (response) => {
if (response.ok) {
setProfile(await response.json());
}
});
}, [sub_username]);
return (
<>
<OrderSets
username={sub_username}
orderSets={orderSets.pools}
permissions={orderSets.permissions}
linkBack={
<Anchor component={Link} to={`/dashboard/`}>
Return to dashboard
</Anchor>
}
/>
{profile ? (
<>
<Title order={1} mb="md">
Sub Profile
</Title>
<ProfileVerification
username={sub_username}
verify_mastodon_alt_text={profile.verify_mastodon_alt_text}
verify_mastodon_favorite={profile.verify_mastodon_favorite}
verify_delay={profile.verify_delay}
/>
{profile.can_edit_permissions ? (
<ProfilePermissions
username={sub_username}
permission_orders_pools_view={
profile.permission_orders_pools_view
}
permission_orders_pools_details={
profile.permission_orders_pools_details
}
permission_orders_pools_edit={
profile.permission_orders_pools_edit
}
/>
) : null}
</>
) : null}
</>
);
};

View file

@ -9,10 +9,6 @@ type UserProfile = {
verify_mastodon_alt_text?: boolean; verify_mastodon_alt_text?: boolean;
verify_mastodon_favorite?: boolean; verify_mastodon_favorite?: boolean;
verify_delay?: number; verify_delay?: number;
can_edit_permissions?: boolean;
permission_orders_pools_view?: boolean;
permission_orders_pools_details?: boolean;
permission_orders_pools_edit?: boolean;
} }
type OrderSetOrderAddOn = { type OrderSetOrderAddOn = {

View file

@ -8,20 +8,16 @@ import {
Input, Input,
Paper, Paper,
Slider, Slider,
Image,
mergeThemeOverrides,
} from "@mantine/core"; } from "@mantine/core";
import { Notifications } from "@mantine/notifications"; import { Notifications } from "@mantine/notifications";
import { mantineTheme } from "./theme";
import "@mantine/core/styles.css"; import "@mantine/core/styles.css";
import "@mantine/dates/styles.css"; import "@mantine/dates/styles.css";
import "@mantine/notifications/styles.css"; import "@mantine/notifications/styles.css";
import "@mantine/charts/styles.css"; import "@mantine/charts/styles.css";
import { Dashboard, subsListLoader } from "./Dashboard"; import { Dashboard, subsListLoader } from "./Dashboard";
import { UserOrderSets, userOrderSetsLoader } from "./UserOrderSets"; import { SubOrderSets, subOrderSetsLoader } from "./SubOrderSets";
import { OrderSet, orderSetLoader, orderSetAction } from "./OrderSet"; import { OrderSet, orderSetLoader, orderSetAction } from "./OrderSet";
import { UserContextProvider } from "./UserContext"; import { UserContextProvider } from "./UserContext";
import { Header } from "./Header"; import { Header } from "./Header";
@ -50,11 +46,6 @@ const theme = createTheme({
}, },
}) as any, }) as any,
}), }),
Image: Image.extend({
defaultProps: {
radius: "xs",
},
}),
}, },
}); });
@ -75,8 +66,8 @@ const router = createBrowserRouter([
}, },
{ {
path: "orders/:username", path: "orders/:username",
Component: UserOrderSets, Component: SubOrderSets,
loader: userOrderSetsLoader, loader: subOrderSetsLoader,
}, },
{ {
path: "orders/:username/new", path: "orders/:username/new",
@ -92,11 +83,9 @@ const router = createBrowserRouter([
}, },
]); ]);
const mergedTheme = mergeThemeOverrides(mantineTheme, theme);
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<MantineProvider defaultColorScheme="dark" theme={mergedTheme}> <MantineProvider theme={theme}>
<UserContextProvider> <UserContextProvider>
<Notifications /> <Notifications />
<RouterProvider router={router} /> <RouterProvider router={router} />

View file

@ -1,78 +0,0 @@
import { Card, Container, createTheme, Paper, rem, Select } from "@mantine/core";
import type { MantineThemeOverride } from "@mantine/core";
const CONTAINER_SIZES: Record<string, string> = {
xxs: rem("200px"),
xs: rem("300px"),
sm: rem("400px"),
md: rem("500px"),
lg: rem("600px"),
xl: rem("1400px"),
xxl: rem("1600px"),
};
export const mantineTheme: MantineThemeOverride = createTheme({
/** Put your mantine theme override here */
fontSizes: {
xs: rem("12px"),
sm: rem("14px"),
md: rem("16px"),
lg: rem("18px"),
xl: rem("20px"),
"2xl": rem("24px"),
"3xl": rem("30px"),
"4xl": rem("36px"),
"5xl": rem("48px"),
},
spacing: {
"3xs": rem("4px"),
"2xs": rem("8px"),
xs: rem("10px"),
sm: rem("12px"),
md: rem("16px"),
lg: rem("20px"),
xl: rem("24px"),
"2xl": rem("28px"),
"3xl": rem("32px"),
},
primaryColor: "orange",
components: {
/** Put your mantine component override here */
Container: Container.extend({
vars: (_, { size, fluid }) => ({
root: {
"--container-size": fluid
? "100%"
: size !== undefined && size in CONTAINER_SIZES
? CONTAINER_SIZES[size]
: rem(size),
},
}),
}),
Paper: Paper.extend({
defaultProps: {
p: "md",
shadow: "xl",
radius: "md",
withBorder: true,
},
}),
Card: Card.extend({
defaultProps: {
p: "xl",
shadow: "xl",
radius: "var(--mantine-radius-default)",
withBorder: true,
},
}),
Select: Select.extend({
defaultProps: {
checkIconPosition: "right",
},
}),
},
other: {
style: "mantine",
},
});